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

上一篇教程的最后,我們可以得到類似下面一張地下城圖:

proc2_1.png

存在兩個問題,一個是存在一些零星的獨立區(qū)域宗收,一個是有些大區(qū)域互不相連。要解決這兩個問題亚兄,要先引入區(qū)域Region的概念混稽,把每個獨立的區(qū)域(包括墻和空白區(qū)域)看做一個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內(nèi)的Coord洽洁,簡單來說,就是遞歸查找格子的上下左右四個相鄰格子菲嘴,直到所有相鄰格子的類型(0表示空地饿自,1表示墻)不同汰翠。

proc2_7.gif

考慮到遞歸方法雖然容易理解,但會大量占用function stack昭雌,在對程序效率要求比較高時還是盡量采取非遞歸的函數(shù)實現(xiàn)复唤。

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的周邊格子烛卧,然后每次加入到返回結(jié)果列表tiles時再次檢查周邊格子佛纫,實現(xiàn)Flood Fill的遞歸。mapTag數(shù)組用來防止重復檢查总放。

有了這個方法呈宇,我們就可以獲取到各個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型豁,則返回所有空白區(qū)域列表,即所有的房間roomRegions尚蝌。

去除小型Region

設定一個thresholdSize值迎变,在獲取所有wallRegionsroomRegions時,如果Region包含的格子數(shù)量小于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;
        }
    }
}

加入這步處理,可以看到上面的地下城圖又規(guī)整了許多:


去除過小region后的地下城

連接房間roomRegions

Flood Fill
獲得的各個Room列表roomRegions

既然獲取到了所有房間的列表姿鸿,接下來就是要遵循一定規(guī)則把所有房間連接起來:

  1. 所有房間與距離最接近的房間相連谆吴。
  2. 確保所有房間互相連接,不會出現(xiàn)獨立的房間苛预。

新建一個Room類句狼,用來抽象房間,里面屬性包含:

  • List<Coord> tiles热某,Room包含的所有點的坐標
  • List<Coord> edgeTiles腻菇,所有Room內(nèi)邊緣的點,即left昔馋,up筹吐,right,down四周有一個點是墻
  • List<Room> connectedRooms秘遏,所有已連接的Room索引

在構(gòu)造函數(shù)中丘薛,計算出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);
}

下面開始構(gòu)造一個函數(shù)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和對應的tileA售睹,tileB
  5. 連接tileA和tileB可训。
  6. 重復步驟1昌妹。

程序?qū)崿F(xiàn)如下:

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窗口可以看到如下圖的路徑被創(chuàng)建:


并沒有完全連接的Room

等等,似乎有哪里不對握截!為什么少了一條路徑飞崖,難道是前面的ConnectClosestRooms方法實現(xiàn)思路有問題?

的確谨胞,再想想的話固歪,會發(fā)現(xiàn)前面的思路可以滿足條件1: 所有房間與距離最接近的房間相連,但無法確保滿足條件2:不會出現(xiàn)獨立的房間胯努。在上面實現(xiàn)的基礎上牢裳,我們需要引入一個新概念:主房間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);
        }
    }
}

加入了上面的處理独郎,最終結(jié)果如下圖所示:


所有房間通過連接到主房間踩麦,確保房間貫通

結(jié)尾

總結(jié)下Room的處理流程:

  1. 用Flood Fill方法選出房間集合和墻區(qū)域的集合枚赡。
  2. 設定閾值,清除過小的墻和房間谓谦。
  3. 獲取房間列表贫橙,兩兩連接最接近房間。
  4. 獲取主房間反粥,確保所有房間都連接到主房間卢肃。

處理好了路徑,接下來的工作就是處理map數(shù)組才顿,把路徑描繪出來莫湘,這個涉及到一些新的概念,可以下一章單獨聊郑气。

本篇內(nèi)容包含Procedural Cave Generation系列視頻教程的第5章幅垮,第6章第7章

源代碼可參考SebLague小哥的Github,戳我直達

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尾组,一起剝皮案震驚了整個濱河市忙芒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讳侨,老刑警劉巖呵萨,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異跨跨,居然都是意外死亡甘桑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門歹叮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跑杭,“玉大人,你說我怎么就攤上這事咆耿〉铝拢” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵萨螺,是天一觀的道長窄做。 經(jīng)常有香客問我,道長慰技,這世上最難降的妖魔是什么椭盏? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮吻商,結(jié)果婚禮上掏颊,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好乌叶,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布盆偿。 她就那樣靜靜地躺著,像睡著了一般准浴。 火紅的嫁衣襯著肌膚如雪事扭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天乐横,我揣著相機與錄音求橄,去河邊找鬼。 笑死葡公,一個胖子當著我的面吹牛罐农,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播匾南,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蛔外!你這毒婦竟也來了蛆楞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夹厌,失蹤者是張志新(化名)和其女友劉穎豹爹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矛纹,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡臂聋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了或南。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孩等。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖采够,靈堂內(nèi)的尸體忽然破棺而出肄方,到底是詐尸還是另有隱情,我是刑警寧澤蹬癌,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布权她,位于F島的核電站,受9級特大地震影響逝薪,放射性物質(zhì)發(fā)生泄漏隅要。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一董济、第九天 我趴在偏房一處隱蔽的房頂上張望步清。 院中可真熱鬧,春花似錦虏肾、人聲如沸尼啡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽崖瞭。三九已至狂巢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間书聚,已是汗流浹背唧领。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留雌续,地道東北人斩个。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像驯杜,于是被迫代替她去往敵國和親受啥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容