程序生成地下城洞穴(2)

上一篇教程的最后,我们可以得到类似下面一张地下城图:

proc2_1.png

存在两个问题,一个是存在一些零星的独立区域,一个是有些大区域互不相连。要解决这两个问题,要先引入区域Region的概念,把每个独立的区域(包括墙和空白区域)看做一个Region,然后针对这些Region进行处理。

用Flood Fill方法获取属于同一Region的格子Grid

先建立一个struct Coord 来抽象格子,里面只有两个属性,X坐标和Y坐标。

struct Coord {
    public int tileX;
    public int tileY;

    public Coord(int x, int y) {
        tileX = x;
        tileY = y;
    }
}

用Flood Fill(可参考Wiki)方式来寻找同一Region内的Coord,简单来说,就是递归查找格子的上下左右四个相邻格子,直到所有相邻格子的类型(0表示空地,1表示墙)不同。

proc2_7.gif

考虑到递归方法虽然容易理解,但会大量占用function stack,在对程序效率要求比较高时还是尽量采取非递归的函数实现。

List<Coord> GetRegionTiles(int startX, int startY) {
    // List to store region tile
    List<Coord> tiles = new List<Coord>();
    // Checked(1) or not(0)
    int[,] mapTag = new int[width, height];
    // Start tile filled or empty
    int tileType = map[startX, startY];

    Queue<Coord> queue = new Queue<Coord>();
    queue.Enqueue(new Coord(startX, startY));
    mapTag[startX, startY] = 1;

    while (queue.Count > 0) {
        Coord tile = queue.Dequeue();
        tiles.Add(tile);
        // Check tile's surrounding tiles
        for (int x = tile.tileX - 1; x <= tile.tileX + 1; x++) {
            for (int y = tile.tileY - 1; y <= tile.tileY + 1; y++) {
                // (x == tile.tileX || y == tile.tileY) just to check four neighbour tiles: up, left, right, down
                if (IsInMapRange(x, y) && (x == tile.tileX || y == tile.tileY)) {
                    if (mapTag[x, y] == 0 && tileType == map[x, y]) {
                        queue.Enqueue(new Coord(x, y));
                        mapTag[x, y] = 1;
                    }
                }
            }
        }
    }

    return tiles;
}

从一个起始点(startX, startY)开始,用一个queue(First In First Out)储存属于同一type的周边格子,然后每次加入到返回结果列表tiles时再次检查周边格子,实现Flood Fill的递归。mapTag数组用来防止重复检查。

有了这个方法,我们就可以获取到各个Region,以及各个Region所包含的所有Coord坐标信息。

List<List<Coord>> GetRegions(int tileType) {
    List<List<Coord>> regions = new List<List<Coord>>();
    // 0 unchecked, 1 checked
    int[,] mapTag = new int[width, height];

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            if (mapTag[x, y] == 0 && map[x, y] == tileType) {
                List<Coord> region = GetRegionTiles(x, y);
                regions.Add(region);
                // Set all tiles to checked
                foreach (Coord tile in region) {
                    mapTag[tile.tileX, tile.tileY] = 1;
                }
            }
        }
    }

    return regions;
}

传入的tileType表示我们要查找的Region属性,如果传入1,则返回所有表示墙的Region列表wallRegions,如果传入0,则返回所有空白区域列表,即所有的房间roomRegions

去除小型Region

设定一个thresholdSize值,在获取所有wallRegionsroomRegions时,如果Region包含的格子数量小于thresholdSize,则消除或填充该Region。

List<List<Coord>> wallRegions = GetRegions(1);
int wallThresholdSize = 50;
foreach (List<Coord> wallRegion in wallRegions) {
    if (wallRegion.Count < wallThresholdSize) {
        foreach (Coord tile in wallRegion) {
            // Set all tiny wall region to empty tile
            map[tile.tileX, tile.tileY] = 0;
        }
    }
}

// Get empty rooms
List<List<Coord>> roomRegions = GetRegions(0);
int roomThresholdSize = 50;
foreach (List<Coord> roomRegion in roomRegions) {
    if (roomRegion.Count < roomThresholdSize) {
        foreach (Coord tile in roomRegion) {
            // Set all tiny room to filled tile
            map[tile.tileX, tile.tileY] = 1;
        }
    }
}

加入这步处理,可以看到上面的地下城图又规整了许多:


去除过小region后的地下城

连接房间roomRegions

Flood Fill
获得的各个Room列表roomRegions

既然获取到了所有房间的列表,接下来就是要遵循一定规则把所有房间连接起来:

  1. 所有房间与距离最接近的房间相连。
  2. 确保所有房间互相连接,不会出现独立的房间。

新建一个Room类,用来抽象房间,里面属性包含:

  • List<Coord> tiles,Room包含的所有点的坐标
  • List<Coord> edgeTiles,所有Room内边缘的点,即left,up,right,down四周有一个点是墙
  • List<Room> connectedRooms,所有已连接的Room索引

在构造函数中,计算出edgeTiles,后面会用它来计算两个房间之间的最短连接路线。

public Room(List<Coord> roomTiles, int[,] map) {
    tiles = roomTiles;
    roomSize = tiles.Count;
    connectedRooms = new List<Room>();

    edgeTiles = new List<Coord>();
    foreach (Coord tile in tiles) {
        for (int x = tile.tileX-1; x <= tile.tileX+1; x++) {
            for (int y = tile.tileY-1; y <= tile.tileY+1; y++) {
                if (x == tile.tileX || y == tile.tileY) {
                    if (map[x,y] == 1) {
                        edgeTiles.Add(tile);
                    }
                }
            }
        }
    }
}

另外加入两个方法ConnectRooms(Room a, Room b)IsConnected(Room other)来把已连接的Room添加到connectedRooms里和检测是否已连接到当前Room。

public static void ConnectRooms(Room roomA, Room roomB) {
    roomA.connectedRooms.Add (roomB);
    roomB.connectedRooms.Add (roomA);
}

public bool IsConnected(Room otherRoom) {
    return connectedRooms.Contains(otherRoom);
}

下面开始构造一个函数ConnectClosestRooms(List<Room> allRooms),Input是所有Room的List,然后用Unity里面Debug.DrawLine方法先画出Room间的连接路径。思路是:

  1. 遍历allRooms,获取roomA,其他所有Room集合称为roomBList。
  2. 遍历roomBList,获取roomB。
  3. 遍历roomA的边缘edgeTiles和roomB的边缘edgeTiles,算出最小距离distanceBetweenRooms和两边的边缘点tileAtileB
  4. 重复步骤2的遍历,直到找到最小的distanceBetweenRooms和对应的tileAtileB
  5. 连接tileA和tileB。
  6. 重复步骤1。

程序实现如下:

void ConnectClosestRooms(List<Room> allRooms) {
    int bestDistance = 0;
    Coord bestTileA = new Coord();
    Coord bestTileB = new Coord();
    Room bestRoomA = new Room();
    Room bestRoomB = new Room();
    bool possibleConnectionFound = false;

    foreach (Room roomA in allRooms) {
        possibleConnectionFound = false;

        foreach (Room roomB in allRooms) {
            if (roomA == roomB) {
                continue;
            }

            if (roomA.isConnected(roomB)) {
                possibleConnectionFound = false;
                break;
            }

            for (int tileIndexA = 0; tileIndexA < roomA.edgeTiles.Count; tileIndexA++) {
                for (int tileIndexB = 0; tileIndexB < roomB.edgeTiles.Count; tileIndexB++) {
                    Coord tileA = roomA.edgeTiles[tileIndexA];
                    Coord tileB = roomB.edgeTiles[tileIndexB];
                    int distanceBetweenRooms = (int)(Mathf.Pow(tileA.tileX - tileB.tileX, 2) + Mathf.Pow(tileA.tileY - tileB.tileY, 2));
                    if (distanceBetweenRooms < bestDistance || !possibleConnectionFound) {
                        bestDistance = distanceBetweenRooms;
                        possibleConnectionFound = true;
                        bestTileA = tileA;
                        bestTileB = tileB;
                        bestRoomA = roomA;
                        bestRoomB = roomB;
                    }
                }
            }
        }

        if (possibleConnectionFound) {
            CreatePassage(bestRoomA, bestRoomB, bestTileA, bestTileB);
        }
    }
}

加入运行后在Scene窗口可以看到如下图的路径被创建:


并没有完全连接的Room

等等,似乎有哪里不对!为什么少了一条路径,难道是前面的ConnectClosestRooms方法实现思路有问题?

的确,再想想的话,会发现前面的思路可以满足条件1: 所有房间与距离最接近的房间相连,但无法确保满足条件2:不会出现独立的房间。在上面实现的基础上,我们需要引入一个新概念:主房间MainRoom

主房间就是所有房间中面积最大的房间,在ConnectClosestRooms处理过后,按照下面流程再做一次处理:

  1. 在建立roomRegions时,选出面积最大的房间mainRoom。
  2. 设定所有与主房间连接的房间为connectedRooms,所有未和主房间连接的房间为otherRooms
  3. 遍历otherRoomsconnectedRooms的边缘tiles,找出一条最短连接路线和两个顶点tileAtileB
  4. 连接tileAtileB,把刚连接的Room加入connectedRooms
  5. 重复步骤2,直到所有房间都和mainRoom相连。
void ConnectClosestRooms(List<Room> allRooms) {
    int bestDistance = 0;
    Coord bestTileA = new Coord();
    Coord bestTileB = new Coord();
    Room bestRoomA = new Room();
    Room bestRoomB = new Room();
    bool possibleConnectionFound = false;

    foreach (Room roomA in allRooms) {
        possibleConnectionFound = false;

        foreach (Room roomB in allRooms) {
            if (roomA == roomB) {
                continue;
            }

            if (roomA.isConnected(roomB)) {
                possibleConnectionFound = false;
                break;
            }

            for (int tileIndexA = 0; tileIndexA < roomA.edgeTiles.Count; tileIndexA++) {
                for (int tileIndexB = 0; tileIndexB < roomB.edgeTiles.Count; tileIndexB++) {
                    Coord tileA = roomA.edgeTiles[tileIndexA];
                    Coord tileB = roomB.edgeTiles[tileIndexB];
                    int distanceBetweenRooms = (int)(Mathf.Pow(tileA.tileX - tileB.tileX, 2) + Mathf.Pow(tileA.tileY - tileB.tileY, 2));
                    if (distanceBetweenRooms < bestDistance || !possibleConnectionFound) {
                        bestDistance = distanceBetweenRooms;
                        possibleConnectionFound = true;
                        bestTileA = tileA;
                        bestTileB = tileB;
                        bestRoomA = roomA;
                        bestRoomB = roomB;
                    }
                }
            }
        }

        if (possibleConnectionFound) {
            CreatePassage(bestRoomA, bestRoomB, bestTileA, bestTileB);
        }
    }
}

加入了上面的处理,最终结果如下图所示:


所有房间通过连接到主房间,确保房间贯通

结尾

总结下Room的处理流程:

  1. 用Flood Fill方法选出房间集合和墙区域的集合。
  2. 设定阈值,清除过小的墙和房间。
  3. 获取房间列表,两两连接最接近房间。
  4. 获取主房间,确保所有房间都连接到主房间。

处理好了路径,接下来的工作就是处理map数组,把路径描绘出来,这个涉及到一些新的概念,可以下一章单独聊。

本篇内容包含Procedural Cave Generation系列视频教程的第5章第6章第7章

源代码可参考SebLague小哥的Github,戳我直达

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容