房間和迷宮:一個地牢生成算法

轉自:https://indienova.com/indie-game-development/rooms-and-mazes-a-procedural-dungeon-generator/

文章來源

微博上的 @大城小胖indienova 個人資料)向我們推薦了這篇文章,在此表示感謝。

本文的作者是:Bob Nystroms,本文的轉載和翻譯已經獲得了他的授權。

原文地址在:Rooms and Mazes: A Procedural Dungeon Generator,有興趣的同學可以前往閱讀原文。閱讀原文是一種良好的習慣,一方面回訪原作者增加他的訪問量,同時也增加他寫文章的動力。另一方面,原文下有些討論的內容會比文章本身還有價值,非常值得常回去看看。

前言

幾個月之前,我承諾要針對之前的一篇針對我的 Roguelike 遊戲的 blog:回合制的遊戲主循環(turn-based game loops)。然後我就投入到我自己發行的新書《遊戲編程模式(Game Programming Patterns)》中去,並且把這件事情徹底忘掉了。我把大家扔在一邊了~~

好,今天我終於有時間再思考一下我的 Roguelike 了,那麼,忘掉遊戲主循環,讓我們來研究一下 Roguelike 遊戲最有趣也是最有挑戰的部分:生成地牢!

點擊下面的這個小方塊看看最後是什麼效果。

再次點擊可以重新開始

看起來很整齊吧?如果你不想深究,那麼代碼在這裏

我關於計算機最早的記憶之一,就是看到我家 Apple IIe 電腦上運行的一個迷宮生成程序。它先將屏幕用綠色的小方塊格子填滿,然後不斷的在牆上挖洞。最後,所有網格上的方塊都被連接起來,屏幕上展現出一個完美、完整的迷宮。

我小小的家用電腦能夠創造這麼神奇的東西--每個方塊都可以同其它的連接--儘管看起來很混亂--它向隨機的方向雕刻,所以每個迷宮都不一樣。這給我十歲的大腦留下了深刻的印象。直到今天也還會有這種感覺。

什麼是地牢?

程序生成(Procedural generation)--讓程序隨機生成取代手工創作--當它工作正常的時候是非常有趣的。由於每一次都不同,你會獲得成倍的重玩樂趣。哪怕作爲一個開發者,你也無法預計自己能否通過自己用程序寫出來的關卡,甚至會出現你意想不到的驚喜。

有時候,使用程序生成會讓人覺得簡單。手工製作顯然需要投入大量的精力和工作。如果你想要一個遊戲有 100 的關卡,那麼你將不得不製作 100 個關卡。但是如果你使用了一個隨機生成關卡的生成器,那麼你將會免費得到 100 關,1000 關,或者 1 百萬關!

哈,當然沒那麼簡單。設計這樣一個程序比你用手工來設計關卡要難得多。你必須要努力動腦,想清楚怎麼做,並且想法子將它轉換成代碼。你其實是要親自編寫一個模擬體系。

它必須要在技術和審美上取得平衡。對我來說,我將注意集中在:

  • 它需要有合理的性能。生成器只需要在進入關卡前運行,所以它不需要非常快。但我們也不希望要讓玩家浪費生命中的好幾秒等待着生成。

  • 地牢需要被連接起來。就像在我綠色屏幕的 Apple 上的迷宮那樣。這意味着在地牢中的任意一點,都有一條道路--哪怕是迂迴的--通往另外一點。

    這是非常重要的。如果玩家領取了一個任務,卻無法到達那裏,會是很殘酷的事情。同時,生成玩家到不了的地方也完全是在浪費時間。

  • 還有,地牢的迷宮應該是不完美的。“完美”的迷宮意味着兩點之間只有唯一的一條通路。所有的走廊分佈得就像一棵樹,它有樹叉,但是中間沒有交集。而“不完美”的迷宮則有着可循環的通路--從 A 到 B 有多個可選通路。

    “不完美”的迷宮是遊戲機制的需要,而不是技術上的需求。你可以造一個基於“完美”的迷宮的 Roguelike,而且確實有不少 Roguelike 是按照這個方法來做的,因爲它的實現比較簡單。

    但是我發現它們玩起來缺少樂趣。當玩家遇到一個死衚衕的時候,必須要回溯回到之前的路線去,然後尋找新的可探索的地方。同時,你也無法繞着敵人轉圈兒,或者走一條小路繞過敵人--針對這些情況,如果能實現的話其實都不賴。因爲從根本上來說:遊戲本來就是一個決定和做出不同選擇的過程。所以,“完美”的地牢只給玩家一條路徑並不太合適。

  • 我需要有開放的房間。我可以創造出沒有房間,完全由狹窄的走廊和過道組成的迷宮。但是這樣玩家就無法真對敵人做出合理的躲避,也無從採取策略來對付敵人,這會喪失很多遊戲樂趣。

    大的、開放的空間可以讓玩家有空間釋放法術,或者進行大型戰鬥。同時,房間也可以通過不同的裝飾風格來增強遊戲場景的表現力。寶箱、陷阱、深淵、藏寶室等等,這些都需要有房間來表現。所以,房間在遊戲中起着至關重要的作用。

  • 我也需要走廊。同時,我也不希望這個地牢完全由房間組成。有些遊戲會將房間連着房間生成。它玩兒起來並沒有什麼問題,但是會有一些乏味。我希望玩家在遊戲過程中有不同的感受,走廊會讓他們感到封閉感,同時,在面對怪獸的時候,將它們引入到狹窄的走廊裏面一個一個幹掉也是一種策略,遊戲體驗會更加豐富。

  • 所有這些應該是可調的。很多 Roguelike 會生成大量的難度逐漸提高的多層地牢,但是除此之外就沒有其它的了。我的遊戲則不是這樣。它有多種不同的區域。每一個區域都有自己的風格和感覺。有些可能很小,感覺很侷促,其它的則可能很寬敞而又井井有條。

    我採用了多種不同的地牢生成算法來實現它。戶外的區域採用完全不同的生成策略(我可能需要針對這個再寫一篇教程。瞧~又一個雄心勃勃的承諾!)但是,從頭開始編寫一個新的地牢生成器會浪費掉大量的時間。所以,理想的做法是將生成器的一些參數設置成可調,這樣我就可以通過同一套代碼生成不同風格和感覺的地牢。

看得見風景的房間

我幾乎一直在開發這個遊戲(還使用了四種不同的語言!),而且我也嘗試了多種不同的地牢生成器。我主要的靈感來源是 Angband。我研究這款遊戲所花的時間要超過我開發自己遊戲所用的時間。

Angband 已經非常老了。在那個時代的電腦上,很難實現快速的地牢生成算法,所以它採用的方法非常簡單:

  1. 隨機生成一批互不覆蓋的房間;
  2. 用隨機的通道將它們連接起來。

爲了確保房間們不覆蓋,我們在每次生成一個新房間的時候,如果發現它跟其它房間有重合,那就丟棄掉。不過這樣可能會造成無限循環,所以我限制了生成房間的總次數。當房間越來越多的時候,生成失敗房間的機率就會越來越高--最後你會發現只能放這麼多房間啦--但是通過調整這個總次數還是會讓你對房間的密度有所控制,比如:

嘗試次數
60

一個黑暗扭曲的走廊

我寫的大部分地牢生成器都是從這兒開始的。但是最難的地方在於:如何將它們連接起來。這也是我這篇教程的主要目的--一個優雅完美的解決方案。

Angband 的解決方案粗暴但是卻令人驚訝的有效。它選擇一對兒房間--完全不管它們距離有多遠--然後從一個房間開始一條隨機的線路連接到另外一個房間(希望能)。它聰明的使用了不少方法來避免過多的交錯和疊加,不過也允許這些線路有機會同房間、走道以及死衚衕交錯。

我花了不少時間試着實現它,但是從未得到理想的結果(應該是我自己的問題)。我生成的走道總是要麼太直,要麼就是交錯得很難看。

然後,幾個月前,我偶然在 reddit 的 /r/roguelikedev 上發現了 u/FastAsUcan 的 地牢生成的描述(description of a dungeon generator),以及他基於 Jamis Buck 地牢生成器的:Karcero。如果你之前有做過地牢生成的代碼,你知道--或者你應該知道--Buck 是誰。他在隨機迷宮生成方面有着大量的文章。

多年以前,我記得他曾經爲紙上 D&D 寫過的一個真的地牢生成器。跟他之前的大多數迷宮不同,這一個有真正的房間,而且結果看上去很棒。

但是,在那時候,我不知道它是怎麼工作的。它是怎樣基於迷宮生成了走廊和房間?我把這個疑問放在腦子的某個角落並且很快忘了它。

FastAsUcan 的帖子提供瞭解答,它大概是這樣工作的:

  1. 創建一個完美的迷宮。有很多算法,都相當直接;
  2. 將迷宮簡化。找到死衚衕並將它們重新用石頭填充;
  3. 選擇一些剩餘死衚衕,然後在毗鄰的牆上打洞,讓它們連接起來。這樣迷宮就不完美了。(記住,這是好事情!)
  4. 創建房間,並且尋找合適的放置處。“合適的”意味着不會覆蓋迷宮,但是要接近迷宮,這樣纔好給它加上門並且連接到走廊。

這裏最神奇的部分,也是我沒想到的部分,就是迷宮的簡化。一個正常的迷宮會把所有區域都覆蓋,不會給你留下任何可以放置房間的空間。Jamis 和 FastAsUcan 所做的則是:先雕刻出迷宮,然後再將死衚衕“反雕刻”回去。

這做起來其實相當容易。死衚衕就是三面都是牆的那個。當你找到一個死衚衕的時候,你將它重置回石塊就可以了。這樣做的結果就是:之前跟它相連的那個走廊塊兒就變成了死衚衕。這樣持續做下去,直到再也找不到死衚衕,你會發現就有了大量可以放置房間的空間。

當然,如果你從一個完美的迷宮開始這麼做,你到最後會將整個迷宮都“簡化”掉!完美的迷宮沒有循環,只要你持續這麼做下去,所有的走廊都會變成死衚衕。Jamis 的解決方案是:不去掉所有的死衚衕,只去掉一些。它運行一會兒就停下來,就像這樣:

保留的過道:
1000

一旦你這麼做了之後,就有機會可以放置房間了。Jamis 採用的方法很有趣,他生成某種尺寸的房間然後試着將它放到迷宮中的每一個位置去,一旦有重合衝突的房間或者走廊,那麼這個位置就被丟棄。剩餘下來的位置會進行一個“排序”,排序的依據是距離走廊越近的排名越高。最後,根據排序結果選擇最好的位置將房間放到那裏。然後再在房間邊上放上門來連接走廊。

不斷的重複這個過程,最後你就得到一個地牢。

房間,然後是迷宮

我立即按照這個方法開始編寫代碼。結果看上去還不錯。但是我發現最後放置房間的步驟顯得非常緩慢。對小面積的紙上游戲來說當然效率還不錯,但是針對基於計算機的 Roguelike 顯然有些喫力。

於是,我做了些思考和修改。在這上面我的貢獻其實很小,但我覺得值得將它記下來。(說實話,我覺得看着地牢動態的生成充滿樂趣)

Buck 和 Karcero 都是先從迷宮開始,然後添加房間。我的則是反其道而行。首先,它會放置一堆房間。然後遍歷整個地牢,當它發現一個完整的開闊空間的時候,從這裏開始生成迷宮。

迷宮生成器持續的雕刻路徑,並且避免同現有的開放的空間交錯。這樣你就可以保證迷宮只有一個解法。如果你讓它雕刻進已有的走廊,那你就會得到循環。

讓你的迷宮填滿房間周圍奇形怪狀的空隙是很方便的。換句話說,迷宮的生成是一個隨機的洪水填充(flood fill) 算法。通過在不連接的空間內進行迷宮的填充,我們就會得到一個地牢:充滿了互相不連接的房間和走廊。

每個顏色代表一個不同的已連接區域

尋找一個連接

現在剩下來的任務就是將所有這些搞成一個連通的地牢。幸運的是,這很容易做到。因爲通過前面的房間和迷宮生成工作,我們可以確定:每一個尚未連接的區域跟它的鄰居之間只相隔一個圖塊(Tile)。

我們可以輕易的找到可能的連接點:

  1. 石塊;
  2. 毗鄰的兩個不同顏色的區域。

高亮顯示的連接點:

我們使用它們來將不同的區域連接起來。通常,我們會將整個地牢看做一個圖(Graph),然後每一個圖塊(Tile)是一個點(Vertex)。但是我們可以將它抽象到更高的層級。現在我們將每個區域看作一個點,而將連接點們當作它們之間的邊。如果我們利用所有的連接點,那麼這個地牢的連通狀態就會變得非常複雜。這並不是我們想要的,所以,我們只需要雕刻那些連接兩個區域的連接點一次。換個漂亮而又專業的說法就是:我們在尋找一個 spanning tree(生成樹)

這個過程相當直接:

  1. 隨機選取一個房間,將它作爲主區域。
  2. 隨機選取一個主區域邊上的連接點並將其打開。在演示中,我們將其變成一個門,但你也可以將其變成一個開放的走廊、上鎖的門或者一個魔法衣櫥。看你怎麼想了,可以充分發揮你的創造力。


    記住,我們並不知道確定的門或者走廊,它們統一表現爲“區域”。所以我們有時可能會直接將兩個房間連接起來。當然你可以避免發生這種情況,但是相信我,這會使得生成的地牢更有趣。

  3. 連接好的區域現在跟主區域是一個整體了,合併成爲新的主區域。在演示中,我用了 flood fill 算法來將新的主區域填充上顏色,因爲這看起來比較醒目。在實際應用中,不需要有這個填充的步驟。只需要創建一個簡單的數據結構來表示“區域 X 已經被合併了”。
  4. 移除掉無關的連接點。在兩個區域合併後,有很大的可能還存在一些兩個區域的連接點。因爲不再需要通過它們來連接這兩個區域了,而且我們需要的是 spanning tree。所以將它們移除掉。
  5. 如果還有剩餘的連接點,那麼再次進行 #2 步的操作。因爲剩餘的連接點表明還有至少一個尚未連接的區域。我們需要循環來將它們合併成最後一個完整的主區域。

前面我說過,我們不需要一個完美的地牢,因爲那會玩起來很無趣。但是,既然我們創建了一個 spanning tree,我們反而得到了一個完美的地牢。我們只允許使用一個連接點來連接兩個區域,於是它變成了一個樹,這意味着地牢中的任意兩點之間只有一條通路。

修正這個問題也很簡單,在 #3 步,當我們準備移除不需要的連接點的時候,我們可以給這些連接點一個很小的機率來變成開放的,就像這樣:

if (rng.oneIn(50)) _carve(pos, CELL_DOOR);

這樣我們就會偶爾的在兩個區域之間多開放一些連接點。這樣我們就會得到有循環、不完美但是更加有趣的地牢。而且這也很容易進行調整,如果我們增加開放額外連接點的機率,那麼我們就會得到連通狀態更加複雜的地牢。

反雕刻

如果我們現在就停下,那麼我們會得到一堆包含了迷宮和走廊的地牢,裏面有很多死衚衕。這看起來很令人抓狂,不過這並不是我們想要的。我們需要簡化它。

我們現在已經將所有房間都互相連接起來了,我們可以移除掉迷宮中的那些死衚衕。我們這樣做的話,只有那些用來連接房間的走廊會被保留。這樣,每一段走廊都會引領玩家去到一個有趣的地方,而不是死衚衕。

最後我們得到什麼

總結一下:

  1. 放置一堆隨機的互相不會覆蓋的房間;
  2. 把房間之外的空地用迷宮填滿;
  3. 將所有相鄰的迷宮和房間連接起來,同時也增加少量的連接;
  4. 移除掉所有的死衚衕。

我很高興我們走到這一步。儘管它並不完美。它致力於在房間之間創造討厭、曲折的走廊。你可以調整自己的地牢算法,但是如果走廊不夠曲折它們就會貼着地牢的邊緣,看起來很奇怪。

房間和迷宮沿着邊界對齊讓事情變得簡單,填充貼合得也比較完美,但是這樣的地牢看起來反而有些人工的痕跡。不過相比我之前的工作它無疑已經有了很大的進步,而且生成的地牢看上去玩起來應該很有趣。

如果你想要自己看看,那麼你可以在瀏覽器中直接玩這個遊戲。演示代碼在這裏,但是相對比較粗糙。我爲了演示而將它們製作成動畫,這增加了代碼的複雜性。所以,這裏是我在遊戲中使用的乾淨的版本。

作爲走到這裏的獎賞,這裏有一個非常巨大的地牢。我覺得非常迷人:

源文件下載

演示版源代碼

前往 github 下載 演示版

乾淨版源代碼

前往 github 下載 乾淨版




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章