如何在 Roguelike 游戲中使用預(yù)制件搭建地下城
英文版:https://github.com/Bozar/DevBlog/wiki/Design_DungeonPrefabs
TROW 備份:https://trow.cc/board/showtopic=51353
引言
Roguelike 游戲中的地下城通常是程序生成的——根據(jù)某些規(guī)則,在隨機(jī)位置生成能夠通過的地面和不能通過的建筑。但有些時候,比起完全依靠代碼隨機(jī)創(chuàng)造地下城,使用手工制作的預(yù)制件更加方便。接下來我想以《再下一層(One More Level)》為例(GitHub[1],在線試玩[2]),談一談如何使用預(yù)制件創(chuàng)建地下城。
本文分為三部分。第一部分介紹了《再下一層》中所使用的預(yù)制件類型,第二部分展示了三個實(shí)例,最后講解了怎樣寫代碼讀取和處理預(yù)制件。
預(yù)制件的定義與分類
本文所指的預(yù)制件是文本文件,文件中的每個字符都屬于一類游戲?qū)ο螅?/p>
地面:玩家人物和非玩家人物能夠停留或通過的格子。
建筑:阻擋通行,有時候人物能夠與建筑交互。
陷阱:可以通行,人物能夠與陷阱交互。
演員:擁有 AI,或者由玩家控制的游戲?qū)ο蟆?/p>
在預(yù)制件里,地面和建筑比陷阱和演員更常見。
預(yù)制件根據(jù)尺寸或“純度”分為不同種類。
尺寸:
全尺寸:用一個預(yù)制件覆蓋全地下城。
拼圖式:一組彼此相鄰的預(yù)制件,拼合起來覆蓋全地下城。
島嶼式:一組彼此不接觸的預(yù)制件,一部分地下城格子沒有被預(yù)制件覆蓋。
“純度”:
純預(yù)制件:僅包含預(yù)制件的地下城。
混合預(yù)制件:包含了預(yù)制件和隨機(jī)生成內(nèi)容的地下城。
接下來介紹三個使用預(yù)制件的場景。
實(shí)例 1:“忍者”地下城
在“忍者”地下城中,玩家人物需要消滅所有忍者才能獲勝。戰(zhàn)斗發(fā)生在狹長的電梯井里,敵人從屏幕頂部出現(xiàn)并下落。我為這座地下城設(shè)計了一個全尺寸預(yù)制件。

Roguelike 游戲的一部分樂趣來自隨機(jī)生成的內(nèi)容。在本地下城里,隨機(jī)性來自演員和陷阱,而不是地面和建筑。忍者出現(xiàn)在隨機(jī)位置,玩家自行決定何時擊殺忍者。忍者死后留下靈魂碎片(陷阱),它能幫助玩家獲勝。另一方面,某些忍者能夠消除靈魂碎片。這個例子表明,哪怕地形固定不變,其它隨機(jī)要素也能提供足夠多的樂趣和挑戰(zhàn)。
實(shí)例 2:“男爵”地下城
這座地下城被等分成若干 7x5 長方塊,每一塊都是一個拼圖式預(yù)制件。生成地下城時,游戲從 25 個候選項(xiàng)里選擇 9 個預(yù)制件,水平翻轉(zhuǎn)、垂直翻轉(zhuǎn)或者保持不變(因此非對稱的預(yù)制件更適合該應(yīng)用場合),最后填入某個隨機(jī)位置。

玩家人物扮演樹上的男爵,找出樹林里的強(qiáng)盜。之所以使用拼圖式預(yù)制件,是因?yàn)楹茈y依靠代碼生成符合規(guī)則的地下城。具體規(guī)則如下。
整張地圖分為兩層:地面層和樹冠層。地面層包括可通行的地面和不可通行的樹樁。所有地面格都是連通的。樹冠層包括樹樁和樹枝。所有樹冠格都可通行,并且連通。
樹枝必須與樹樁相鄰。樹樁能夠獨(dú)立存在,也可與任意對象相鄰。玩家人物能夠在樹樁上等待一輪,但是不能在樹枝上停留。
強(qiáng)盜在地面層行動,不能爬樹。玩家人物和鳥停留在樹冠層,不能下到地面。對玩家而言,鳥始終可見,強(qiáng)盜僅當(dāng)距離較近、并且未被樹枝遮擋時可見。
為了確保格子的連通性,每個預(yù)制件要滿足兩個額外條件:
超過半數(shù)的邊緣格子是地面或樹枝。
超過半數(shù)的邊緣格子是樹樁或樹枝。
為了提高游戲難度,樹枝多一些比較好,因?yàn)閺?qiáng)盜能夠躲在樹枝下面,而且玩家人物沒法停留在樹枝上。另一方面,樹樁太多了會降低難度,應(yīng)該盡量避免。
由于拼圖式預(yù)制件比整個地下城小,怎樣設(shè)計出一系列既滿足上述要求、而且變化盡可能多的預(yù)制件?我的方法是先定義類別,再分類設(shè)計。根據(jù)樹冠層在邊緣的連通性,“男爵”地下城的預(yù)制件分為五類。
A 類:所有邊緣樹冠格都是連通的。
B 類:每條邊至少有一個孤立的樹冠格。
C 類:所有水平的邊緣樹冠格都是連通的。
D 類:所有垂直的邊緣樹冠格都是連通的。
E 類:恰好有 1 條或 3 條邊緣的樹冠格是連通的。

實(shí)例 3:“工廠”地下城
工廠由不同尺寸的廠房組成,它們是島嶼式預(yù)制件。
20 個大廠房:7x7 格。
2 個小廠房:3x3 格。
2 個起始廠房:3x3 格。
1 個門框:3x1 格。
每座地下城有一個起始廠房,至少兩個大廠房,若干小廠房和門框。它們被隨機(jī)選擇出來,翻折、旋轉(zhuǎn)或保持不變,最后放入某個位置,與其它建筑至少距離 1 格。

我希望用島嶼式預(yù)制件生成一座小范圍有序、大范圍無序的地下城。因此所有廠房都用手工設(shè)計,但是它們的位置由程序隨機(jī)決定。
兩棟建筑之間一格寬的過道確保了門不會被建筑遮擋??紤]到建筑有可能緊貼地圖邊緣,我給大廠房的三面外墻分別添加了一扇門。
大廠房根據(jù)缺失部分的面積分為六類:0x0,2x2,2x3,3x3,2x4,4x4。某一類大廠房可以根據(jù)內(nèi)部房間的形狀(一格寬走廊或長方形)進(jìn)一步分類。

寫代碼讀取、修改和使用預(yù)制件
把預(yù)制件用于生成地下城涉及到四個步驟。
設(shè)計預(yù)制件。
寫代碼讀取預(yù)制件(文本文件)。
根據(jù)需要修改預(yù)制件。
根據(jù)預(yù)制件數(shù)據(jù)生成地下城。
我用 REXPaint[3] 制作預(yù)制件,然后導(dǎo)出文本文件。《再下一層》使用的預(yù)制件存放在兩個目錄中:*.xp 文件[4],*.txt 文件[5]。
第二步是打開并讀取文本文件,隨后輸出文件內(nèi)容。這些操作與具體的程序語言有關(guān),沒有通用方法。我使用一個自定義函數(shù) `read_as_line(path_to_file: String) -> Game_FileParser`。文件內(nèi)容存儲在字典里(`FileParser.output_line`),字典的鍵是行號,值是字符串。詳見兩個腳本:FileIOHelper.gd[6],F(xiàn)ileParser.gd[7]。
修改預(yù)制件分成兩部分,詳見 DungeonPrefab.gd[8]。首先,為了獲取預(yù)制件中的某個字符,可以通過 `FileParser.output_line[line_number][string_index]`,這等同于 `FileParser.output_line[y][x]`,見圖 6。不過我希望使用 `[x][y]` 獲取字符,因此創(chuàng)建了一個新字典,令 `new_dict[x][y] = FileParser.output_line[y][x]`。
圖 6:預(yù)制件中的字符。
上文說過,預(yù)制件可能被翻折或旋轉(zhuǎn),因此需要三個函數(shù):`_horizontal_flip() -> Dictionary`,`_vertical_flip() -> Dictionary` 和 `_rotate_right() -> Dictionary`。
翻折很簡單。`_horizontal_flip()` 的核心部分如下。
為了把預(yù)制件順時針旋轉(zhuǎn) 90 度,首先把它繞著原點(diǎn)旋轉(zhuǎn),隨后向右平移,見下圖。

`_rotate_right()` 的核心部分如下。
最后一步(根據(jù)預(yù)制件數(shù)據(jù)生成地下城)與實(shí)際游戲結(jié)合緊密,因此超出了文章的討論范圍。本文到此結(jié)束。

[1] https://github.com/Bozar/OneMoreLevel
[2] https://trow.cc/board/showtopic=50608
[3] https://www.gridsagegames.com/rexpaint/
[4] https://github.com/Bozar/OneMoreLevel/tree/master/resource/REXPaint
[5] https://github.com/Bozar/OneMoreLevel/tree/master/resource/dungeon_prefab
[6] https://github.com/Bozar/OneMoreLevel/blob/master/library/FileIOHelper.gd
[7] https://github.com/Bozar/OneMoreLevel/blob/master/library/FileParser.gd
[8] https://github.com/Bozar/OneMoreLevel/blob/master/library/DungeonPrefab.gd