在Unity中程式化生成的地牢環境
我非常喜歡Roguelike類遊戲,而且Roguelike類遊戲的關卡往往由許多地牢環境組成,所以我萌生出嘗試自己編寫程式化地牢生成器的想法。
地牢生成器有很多不同的實現方法,但我最後決定使用《TinyKeep》遊戲的演算法。通過擴充套件該演算法,我使它可以在3D環境中使用,建立出帶有多個樓層的地牢。
示例程式碼
下載示例的程式碼:
https://github.com/vazgriz/DungeonGenerator
瞭解《TinyKeep》遊戲的演算法:
https://www.reddit.com/r/gamedev/comments/1dlwc4/procedural_dungeon_generation_algorithm_explained/
2D環境
首先,我必須編寫適用於2D環境的演算法。我的演算法和《TinyKeep》遊戲的演算法大致相同,但進行了一些調整,使演算法可以實現更加有趣的關卡。
本文中示例場景的名稱為Dungeon2D,程式碼儲存在Scripts2D資料夾。
演算法詳解
在專案中,遊戲世界被劃分為矩形網格。假設1個單位的寬度足以表示一個通道。例如:在一個遊戲中,1個單位可能對應著5米的距離。因此,我最後決定把遊戲世界的大小設為30×30。
1、首先隨意放置地牢區域,確保它們不會互相重疊。這些區域的佈置方式並不重要,在本文示例中,我只是隨機放置並調整了它們的大小。
我還在每個面新增了1個單位寬度的緩衝區,從而確保各個區域不會互相接觸,但這對演算法來說並不必要。
紅色方框是各個地牢區域
2、使用所有地牢區域建立Delaunay三角剖分圖。我使用了Bowyer-Watson演算法,該演算法有不同語言的多種實現方法。我選擇了最容易轉化為C#程式碼的實現方法。
Delaunay三角剖分效果
3、 使用三角剖分圖建立最小生成樹,我在此使用了Prim演算法。
通道的最小生成樹
4、使用第3步中的每個邊緣,建立通道的列表。生成樹包含每個區域,因此它可以保證有通向每個區域的通道。
通過三角剖分圖隨機新增邊緣到列表中。這樣會把通道連成一圈,在這裡,我把每個邊緣的新增機率設為12.5%。
給生成樹新增邊緣之後的通道
5、對於列表中的每個通道,使用了A*演算法來尋找從通道開始到結束的路徑。在找到一條路徑後,它會修改遊戲世界的狀態,從而讓之後的通道可以使用現有通道的路徑。
通過使用特別的代價函式,我讓演算法可以使用之前迭代的結果,該方法的開銷比建立新通道的開銷更低。這樣可以讓尋路程式使用組合通道穿過相同的區域。
雖然尋路程式可以穿過區域,但是開銷較大。因此在大多數情況下,尋路程式更傾向於繞著區域走。
藍色方塊是通道
下圖是使用了美術資源實現的示例場景,美術資源和放置資源的程式碼不包含在示例程式碼中。
3D環境
有了2D環境的地牢生成器後,我開始把生成器轉變為適用於3D環境。我們使用的所有演算法都有3D版本,因此轉換過程不會很困難。
演算法詳解
現在,我們的網格大小設定為30x5x30。
1、首先要修改的是:讓所有地牢區域在3D環境中生成。
請注意地牢區域有可能有多個樓層
2、計算地牢區域的3D Delaunay三角剖分結果,或改為計算Delaunay四面體剖分結構。
然而,我在網上雖然可以搜尋到很多研究論文,但沒有找到任何實際的程式碼。最接近專案情況的結果是CGAL的3D三角剖分實現方法,但是該方法存在兩個問題:
該功能模組只可以在GPL許可下使用。
程式碼有大量模板部分,而且非常難以理解,導致我無法找到實現演算法的具體位置。
最後,我不得不學習Bowyer-Watson演算法的工作原理,以便自己修改程式碼。
雖然,我並不理解外接圓對該演算法的重要性,但至少我可以參考Wolfram MathWorld網站相關頁面的資訊,在編寫的程式碼中使用了外接球體。由於大部分運算是4×4矩陣運算,我使用了Unity的Matrix4x4結構來處理運算過程。
MathWorld網站的相關頁面:
http://mathworld.wolfram.com/Circumsphere.html
新的程式碼實現版本在Scripts3D/Delaunay3D.cs指令碼中,如果需要基於MIT許可且易於理解的版本,可以參考這個指令碼。
這種方法不會生成帶有3個頂點的三角形,而是會生成帶有4個頂點的四面體。其中至少有一個頂點會位於不同的樓層,否則該四面體會被銷燬。這為尋路過程提供了很多在不同樓層之間移動的機會。
3、現在到了2D版本的對應第3步和第4步。我簡單修改第2步的邊緣資訊,然後把它傳入Prim演算法。
通道的3D生成樹
重新新增了帶有邊緣的通道
4、3D版本的A*演算法是情況變得複雜的地方。2D版本A*演算法非常簡單,只要使用A*演算法的標準實現方法即可。但為了實現3D版本,我必須給尋路程式新增向上和向下移動的功能,讓它可以連線不同樓層的區域。
我決定使用樓梯臺階來連線樓層,而不是垂直的梯子。但這也是問題出現的地方。相對於垂直上下移動的梯子來說,臺階更加複雜。臺階必須通過水平移動,來實現垂直移動,也就是說,要結合上升和前進的過程。我們可以通過下圖來理解這個過程。
中心網格是藍色實心正方形。附近的網格都是空心的正方形。尋路程式不能直接移動到當前網格正上方的網格,而是必須同時在水平方向和垂直方向移動。
如下圖所示,這是從側面檢視的檢視。我們可以直接移動到水平方向的相鄰位置,但是無法直接去到頂部位置。
為了針對臺階構建尋路程式,我必須選好臺階的形狀。如果把垂直移動和水平移動的距離比設為1:1,樓梯可能會太陡,因此我把比值設為1:2。每當在垂直方向移動1個單位時,尋路程式都要在水平方向移動2個單位。
此外,我們也要有合適的天花板,讓角色可以站在臺階上,因此臺階上也必須有2個網格的高度。總的來說,每個臺階會使用4個網格。
樓梯臺階和上方淨空間
此外,樓梯的頂部和底部必須有合適的通道。尋路程式不可以從側面或相反方向進入樓梯。如果讓樓梯直接穿過通道,則會產生非常奇怪的情況,如下圖所示。
因此,樓梯形狀必須如下圖所示,尋路程式必須確保通道位於兩個藍色正方形的位置。如下圖所示,樓梯會從藍色實心正方形開始,向上移動一個樓層。
尋路程式必須在一步中從開始位置移動到結束位置。這意味著它必須在水平方向移動3個單位,在垂直方向移動1個單位。
A*演算法的原理是每一步都會從一個節點移動到鄰近節點。為了製作樓梯,我需要“跳過”樓梯的四個網格。
這裡的難點在於:我需要讓尋路程式圍繞著它建立的樓梯工作。我無法把樓梯新增到A*演算法的封閉集合中,因為那樣會阻礙不同的路徑從其它方向穿過這些網格。
但我也不可以把它們排除在外,因為這樣會使尋路系統可以在最初建立的樓梯移動,出現上圖片中不合理的情況。
解決方法是:對於每個節點,我們要跟蹤該路徑上的之前節點,然後在評估相鄰節點時,如果遇到當前節點的路徑,尋路系統會拒絕生成路徑。
樓梯末端的通道會包含樓梯佔用的所有網格、樓梯起點的節點和之前路徑的所有節點。
尋路程式可以建立另一條路徑穿過樓梯,因為第二條路徑不瞭解樓梯的情況。
以上行為只需呼叫一次尋路函式,就可以處理多條潛在的路徑。我們可以多次呼叫尋路函式,生成所有通道。之後的迭代會圍繞已有的樓梯進行處理。
現在使用的演算法不完全是A*演算法。為了處理好樓梯,我們要應對許多特別情況。但如果每一步都檢查之前的整條路徑,這樣會費時費力。
最初的實現方法是一路跟隨節點到起點位置,像連結串列一樣讀取節點。因此,對於每個相鄰節點來說,檢查路徑的時間複雜度為O(N)。
我使用的實現方法是:在每個節點中儲存一個雜湊表,該雜湊表使用之前的節點作為鍵值。因此,檢查路徑的時間複雜度為O(1),但是在節點新增到路徑時,雜湊表必須進行拷貝,使用的時間複雜度為O(N)。
我選擇這個方法的原因是:我發現該演算法讀取路徑的頻率比修改路徑的頻率更高。
雖然我不知道如何對它的時間複雜度進行分析,但我認為總體時間複雜度應該在O(N^2)左右。這項改動是地牢生成演算法的主要瓶頸。
在完成所有改動後,實現的結果如下圖所示。
綠色方框表示樓梯
生成器建立的路徑可以很簡單
也可以很複雜
下面是使用美術資源實現的3D地牢效果。
有多個樓層的地牢
兩個樓梯組成兩倍寬度的樓梯
三倍寬度的樓梯
下降兩個樓層的路徑可以建立互相銜接的樓梯
出現多個互相靠近的路徑時,通道可能會變得很大
兩個樓梯連線相同的門
結語
本文分享了在Unity中程式化生成2D和3D的地牢環境的方法。專案最難的部分是實現適用於3D環境的演算法以及編寫編寫尋路程式。
建立的地牢環境非常有趣,可以作為一款遊戲的基礎,來動手嘗試一下吧。
作者:vazgriz
來源:Unity官方平臺
原地址:https://mp.weixin.qq.com/s/3yM-mAAXq_fX5tcy82s0uQ
相關文章
- 洞窟類地牢生成
- 配置開發環境、生成環境、測試環境開發環境
- 生成Dll在Unity中使用Unity
- 【Unity + Google Cardboard】 VR環境配置UnityGoVR
- Anaconda與Python環境在Windows中的部署PythonWindows
- Python GDAL庫在Anaconda環境中的配置Python
- JavaScript在瀏覽器環境中的非同步JavaScript瀏覽器非同步
- 在Grammarly的生產環境中執行LispLisp
- 專案經理在敏捷環境中的作用敏捷
- 在 Visual Studio Code 中配置 Python Flask 環境PythonFlask
- Android開發:在Eclipse中配置Android環境AndroidEclipse
- 在Mac OS中搭建superset開發環境Mac開發環境
- 如何在Ubuntu 20.04上安裝Unity桌面環境UbuntuUnity
- 生成環境之Nginx高可用方案Nginx
- 在UE4中建立受《羞辱》啟發的環境
- 在nodejs環境裡使用瀏覽器環境下的document物件NodeJS瀏覽器物件
- Haskell 在 macOS 下的環境搭建HaskellMac
- 8.4.9 在truffle環境中執行外部指令碼指令碼
- 在 kubernetes 環境中實現 gRPC 負載均衡RPC負載
- 在離線環境中安裝Visual Stuido 2017UI
- 人工智慧中的情景環境與順序環境人工智慧
- Dubbo Mesh 在閒魚生產環境中的落地實踐
- STARTTLS在電子郵件環境中的安全性分析TLS
- 在Hadoop環境中,大資料儲存的技巧有哪些?Hadoop大資料
- 在Linux中,什麼是環境變數?如何設定和檢視環境變數?Linux變數
- java中Hibernate的環境配置Java
- HMMer在Windows環境下的安裝HMMWindows
- zerorunner 在 Centos 的環境配置和部署CentOS
- Inmp-(2)在ubuntu18.04中搭建lnmp環境UbuntuLNMP
- 探秘Kubernetes:在本地環境中玩轉容器技術
- 4.2.10 在Oracle重啟配置中管理環境變數Oracle變數
- 4.2.12 在 Oracle Restart Environment 環境中啟用 FAN 事件OracleREST事件
- TensorRT 筆記 - 在 Conda 虛擬環境中安裝筆記
- 在Unity中渲染一個黑洞Unity
- hanlp在Python環境中的安裝失敗後的解決方法HanLPPython
- 如何才能防止小程式在激烈的市場環境中不被出局
- 00 在Windows環境中開發Cordova專案的準備工作Windows
- paddleocr 在docker環境下部署Docker