游戏简介
地牢生成(Dungeon Generation)是Roguelike游戏的核心技术之一。与迷宫生成不同,地牢通常由一系列随机分布的房间和连接它们的走廊组成,形成更自然、更具探索感的地下空间。本文将用Java实现一套完整的地牢生成系统,讲解BSP(Binary Space Partitioning,二叉空间分割)算法、房间放置策略、走廊连接算法,以及基于图论的最小生成树优化。
核心设计思路
一个典型的地牢生成流程包括四个阶段:
- 空间分割:使用BSP将大矩形区域递归分割成若干小区域
- 房间生成:在每个叶节点区域内随机放置房间
- 走廊连接:在相邻区域之间创建走廊,确保所有房间连通
- 图优化:用最小生成树去除冗余走廊,同时保留少量环路增加探索性
BSP二叉空间分割
BSP算法递归地将矩形区域沿水平或垂直方向分割,直到每个区域的大小低于阈值。分割时引入随机性,使地牢更具变化。
import java.util.*;
public class BSPTree {
/**
* BSP节点,表示一个矩形区域
*/
public static class Node {
public int x, y, width, height; // 区域位置和大小
public Node left, right; // 左右子节点
public Room room; // 叶节点上的房间
public Node(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public boolean isLeaf() {
return left == null && right == null;
}
}
/**
* 房间定义
*/
public static class Room {
public int x, y, width, height;
public int centerX, centerY;
public Room(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.centerX = x + width / 2;
this.centerY = y + height / 2;
}
}
private static final int MIN_LEAF_SIZE = 10; // 叶节点最小尺寸
private static final int MAX_LEAF_SIZE = 24; // 超过此尺寸继续分割
private static final Random RANDOM = new Random();
/**
* 构建BSP树
*/
public static Node buildTree(int rootX, int rootY, int rootWidth, int rootHeight) {
Node root = new Node(rootX, rootY, rootWidth, rootHeight);
List<Node> leaves = new ArrayList<>();
leaves.add(root);
boolean split = true;
while (split) {
split = false;
List<Node> newLeaves = new ArrayList<>();
for (Node leaf : leaves) {
if (leaf.isLeaf()) {
// 如果区域太大,尝试分割
if (leaf.width > MAX_LEAF_SIZE || leaf.height > MAX_LEAF_SIZE) {
if (splitNode(leaf)) {
newLeaves.add(leaf.left);
newLeaves.add(leaf.right);
split = true;
continue;
}
}
}
newLeaves.add(leaf);
}
leaves = newLeaves;
}
// 在所有叶节点上生成房间
for (Node leaf : leaves) {
leaf.room = generateRoom(leaf);
}
return root;
}
/**
* 分割节点,随机选择水平或垂直分割
*/
private static boolean splitNode(Node node) {
// 决定分割方向:如果宽度大于高度150%,优先垂直分割;反之优先水平分割
boolean splitH = RANDOM.nextBoolean();
if (node.width > node.height * 1.5) {
splitH = false; // 垂直分割
} else if (node.height > node.width * 1.5) {
splitH = true; // 水平分割
}
int maxSize = splitH ? node.height : node.width;
if (maxSize < MIN_LEAF_SIZE * 2) {
return false; // 太小,无法分割
}
// 随机选择分割位置(至少留MIN_LEAF_SIZE给两边)
int splitPos = MIN_LEAF_SIZE + RANDOM.nextInt(maxSize - MIN_LEAF_SIZE * 2 + 1);
if (splitH) {
// 水平分割(上下)
node.left = new Node(node.x, node.y, node.width, splitPos);
node.right = new Node(node.x, node.y + splitPos, node.width, node.height - splitPos);
} else {
// 垂直分割(左右)
node.left = new Node(node.x, node.y, splitPos, node.height);
node.right = new Node(node.x + splitPos, node.y, node.width - splitPos, node.height);
}
return true;
}
/**
* 在叶节点内随机生成房间,四周留走廊空间
*/
private static Room generateRoom(Node leaf) {
// 房间四周至少留1格作为墙/走廊空间
int padding = 2;
int maxRoomWidth = leaf.width - padding * 2;
int maxRoomHeight = leaf.height - padding * 2;
if (maxRoomWidth < 3 || maxRoomHeight < 3) {
// 区域太小,直接填满
return new Room(leaf.x + 1, leaf.y + 1, leaf.width - 2, leaf.height - 2);
}
int roomWidth = 3 + RANDOM.nextInt(maxRoomWidth - 2);
int roomHeight = 3 + RANDOM.nextInt(maxRoomHeight - 2);
int roomX = leaf.x + padding + RANDOM.nextInt(maxRoomWidth - roomWidth + 1);
int roomY = leaf.y + padding + RANDOM.nextInt(maxRoomHeight - roomHeight + 1);
return new Room(roomX, roomY, roomWidth, roomHeight);
}
}
走廊连接算法
生成房间后,需要在房间之间建立走廊。最常用的方法是连接兄弟节点的房间,然后递归向上连接。走廊可以是L形(先水平后垂直,或先垂直后水平)。
public class DungeonGenerator {
public static final int TILE_WALL = 0;
public static final int TILE_FLOOR = 1;
public static final int TILE_CORRIDOR = 2;
public static final int TILE_DOOR = 3;
private int[][] map;
private int width, height;
private List<BSPTree.Room> rooms;
private List<Corridor> corridors;
public DungeonGenerator(int width, int height) {
this.width = width;
this.height = height;
this.map = new int[height][width];
this.rooms = new ArrayList<>();
this.corridors = new ArrayList<>();
// 初始化为墙
for (int y = 0; y < height; y++) {
Arrays.fill(map[y], TILE_WALL);
}
}
/**
* 使用BSP生成地牢
*/
public void generate() {
// 1. BSP分割
BSPTree.Node root = BSPTree.buildTree(1, 1, width - 2, height - 2);
// 2. 收集所有房间
collectRooms(root);
// 3. 在BSP树上连接兄弟房间
connectRooms(root);
// 4. 渲染到地图
renderRooms();
renderCorridors();
}
private void collectRooms(BSPTree.Node node) {
if (node == null) return;
if (node.isLeaf() && node.room != null) {
rooms.add(node.room);
} else {
collectRooms(node.left);
collectRooms(node.right);
}
}
/**
* 递归连接兄弟节点的房间
*/
private void connectRooms(BSPTree.Node node) {
if (node == null || node.isLeaf()) return;
// 先递归连接子树内部
connectRooms(node.left);
connectRooms(node.right);
// 连接左右子树的最近房间
BSPTree.Room roomA = findNearestRoom(node.left, node.right.room);
BSPTree.Room roomB = findNearestRoom(node.right, node.left.room);
if (roomA != null && roomB != null) {
createCorridor(roomA, roomB);
}
}
/**
* 找到子树中距离目标房间最近的房间
*/
private BSPTree.Room findNearestRoom(BSPTree.Node node, BSPTree.Room target) {
if (node == null) return null;
if (node.isLeaf() && node.room != null) {
return node.room;
}
BSPTree.Room leftRoom = findNearestRoom(node.left, target);
BSPTree.Room rightRoom = findNearestRoom(node.right, target);
if (leftRoom == null) return rightRoom;
if (rightRoom == null) return leftRoom;
double leftDist = distance(leftRoom, target);
double rightDist = distance(rightRoom, target);
return leftDist < rightDist ? leftRoom : rightRoom;
}
private double distance(BSPTree.Room a, BSPTree.Room b) {
return Math.sqrt(Math.pow(a.centerX - b.centerX, 2) + Math.pow(a.centerY - b.centerY, 2));
}
/**
* 创建L形走廊连接两个房间
*/
private void createCorridor(BSPTree.Room a, BSPTree.Room b) {
int startX = a.centerX;
int startY = a.centerY;
int endX = b.centerX;
int endY = b.centerY;
// 随机决定先水平还是先垂直
if (RANDOM.nextBoolean()) {
// 先水平后垂直
corridors.add(new Corridor(startX, startY, endX, startY, true));
corridors.add(new Corridor(endX, startY, endX, endY, false));
} else {
// 先垂直后水平
corridors.add(new Corridor(startX, startY, startX, endY, false));
corridors.add(new Corridor(startX, endY, endX, endY, true));
}
}
/**
* 将房间渲染到地图
*/
private void renderRooms() {
for (BSPTree.Room room : rooms) {
for (int y = room.y; y < room.y + room.height; y++) {
for (int x = room.x; x < room.x + room.width; x++) {
if (y >= 0 && y < height && x >= 0 && x < width) {
map[y][x] = TILE_FLOOR;
}
}
}
}
}
/**
* 将走廊渲染到地图
*/
private void renderCorridors() {
for (Corridor c : corridors) {
if (c.horizontal) {
int minX = Math.min(c.x1, c.x2);
int maxX = Math.max(c.x1, c.x2);
for (int x = minX; x <= maxX; x++) {
if (c.y1 >= 0 && c.y1 < height && x >= 0 && x < width) {
map[c.y1][x] = TILE_CORRIDOR;
}
}
} else {
int minY = Math.min(c.y1, c.y2);
int maxY = Math.max(c.y1, c.y2);
for (int y = minY; y <= maxY; y++) {
if (y >= 0 && y < height && c.x1 >= 0 && c.x1 < width) {
map[y][c.x1] = TILE_CORRIDOR;
}
}
}
}
}
static class Corridor {
int x1, y1, x2, y2;
boolean horizontal;
Corridor(int x1, int y1, int x2, int y2, boolean horizontal) {
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
this.horizontal = horizontal;
}
}
public void print() {
char[] symbols = {'#', '.', '+', 'D'};
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
System.out.print(symbols[map[y][x]]);
}
System.out.println();
}
}
public int[][] getMap() {
return map;
}
public List<BSPTree.Room> getRooms() {
return rooms;
}
}
最小生成树优化冗余走廊
BSP递归连接会生成大量冗余走廊。我们可以通过构建房间连通图,计算最小生成树(MST)来保留必要连接,同时额外添加少量边形成环路,增加地牢的探索性和非线性。
public class DungeonOptimizer {
/**
* 房间连通图的边
*/
static class Edge implements Comparable<Edge> {
int from, to;
double weight;
Edge(int from, int to, double weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
@Override
public int compareTo(Edge other) {
return Double.compare(this.weight, other.weight);
}
}
/**
* 使用Kruskal算法计算最小生成树,并额外添加环路边
* @param rooms 房间列表
* @param extraEdgeRatio 额外环路边的比例(0.0-1.0)
* @return MST边 + 额外边
*/
public static List<Edge> optimizeConnections(List<BSPTree.Room> rooms, double extraEdgeRatio) {
int n = rooms.size();
List<Edge> allEdges = new ArrayList<>();
// 构建完全图
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
double dist = distance(rooms.get(i), rooms.get(j));
allEdges.add(new Edge(i, j, dist));
}
}
Collections.sort(allEdges);
// Kruskal算法
UnionFind uf = new UnionFind(n);
List<Edge> mst = new ArrayList<>();
List<Edge> nonMst = new ArrayList<>();
for (Edge e : allEdges) {
if (uf.find(e.from) != uf.find(e.to)) {
uf.union(e.from, e.to);
mst.add(e);
} else {
nonMst.add(e);
}
}
// 添加额外环路边
int extraCount = (int) (nonMst.size() * extraEdgeRatio);
Collections.shuffle(nonMst);
for (int i = 0; i < Math.min(extraCount, nonMst.size()); i++) {
mst.add(nonMst.get(i));
}
return mst;
}
private static double distance(BSPTree.Room a, BSPTree.Room b) {
return Math.sqrt(Math.pow(a.centerX - b.centerX, 2) + Math.pow(a.centerY - b.centerY, 2));
}
/**
* 并查集
*/
static class UnionFind {
int[] parent;
UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void union(int x, int y) {
parent[find(x)] = find(y);
}
}
}
完整可运行示例
public class DungeonMain {
public static void main(String[] args) {
int dungeonWidth = 80;
int dungeonHeight = 40;
DungeonGenerator generator = new DungeonGenerator(dungeonWidth, dungeonHeight);
generator.generate();
System.out.println("=== 生成的地牢 ===");
generator.print();
System.out.println("\n房间数量: " + generator.getRooms().size());
// 使用MST优化连接
List<DungeonOptimizer.Edge> optimized = DungeonOptimizer.optimizeConnections(
generator.getRooms(), 0.15); // 15%额外环路
System.out.println("优化后边数: " + optimized.size());
}
}
复杂度分析
| 阶段 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| BSP分割 | O(n),n为节点数 | O(n) |
| 房间生成 | O(n) | O(n) |
| 走廊生成 | O(n log n) | O(n) |
| MST优化(Kruskal) | O(V² log V),V为房间数 | O(V²) |
| 地图渲染 | O(W×H),W/H为地图宽高 | O(W×H) |
总结
本文实现了一套完整的地牢生成系统,核心亮点包括:
- BSP二叉空间分割:递归随机分割确保地牢布局的自然多样性和层次结构
- 房间与走廊生成:在叶节点内放置房间,通过L形走廊连接兄弟节点,保证连通性
- 最小生成树优化:使用Kruskal算法去除冗余连接,在保持连通的前提下降低走廊密度
- 环路控制:通过
extraEdgeRatio参数精确控制额外环路比例,平衡线性和非线性探索体验
读者可以进一步扩展,例如添加门、陷阱、宝箱等元素的随机放置,或引入Perlin噪声进行地形细节雕刻,打造更丰富的地下世界。