最近幾日閑來無事,后來看到了RogueLike的游戲,就像來試一下地牢生成算法。
網上看到了一篇文章寫的挺好的。后面會有轉載,不急哈。
先看一下我實現的效果圖
生成過程:
地牢生成算法的思路是:
1.生成大量隨機位置隨機大小的房間 2.通過物理碰撞將房間分散開 3.通過閾值選取主要的房間 4.對主房間進行三角剖分運算,得到房間之間的可選路徑 5.使用最小生成樹選定房間之間的路徑,並且保留少量回圈 6.確定房間路路線 7.路上經過的房間划入次要房間中 8.繪制房間和路線.
隨機生成房間
需要隨機生成房間的初始位置,以及房間的長寬高
文章中提供了一種思路,在圓形中隨機確定一個點,作為房間的初始位置。
而如何在圓形中隨機生成一個點。
圓中生成隨機點
先從一個三角形中入手
假設三角形ABC 且|AB| =|AC|。
在AB中找一個點X,在AC中找一個點Y,並用AX和AY做一個平行四邊形XAYZ。那么這個隨機點Z就找到了。
那萬一Z超出了三角形呢
沒事,超出了三角形,就將點Z以BC翻折到三角形中。
那我們該怎么運用在圓中呢?
其實這里去了一個極限,將一個圓細分成無數的等腰三角形
這樣,我們只要在圓中選擇一個三角形,再從三角形中隨機選擇一個點。
這樣就能隨機生成一個點了。
現在輪到房間大小了
房間大小可以利用上面的方法,在圓內生成一個點,再用這個點生成一個矩形。
可以有兩種生成方法,如下圖。
分散房間
這里可以選擇使用Unity中自帶的Rigidbody分散,
因為Unity的碰撞不好控制,常常跑出來一堆小數,所以我選擇自己寫一個Rigidbody。
邏輯非常簡單,只需要判斷自己是否覆蓋別人的房間,如果覆蓋就移動到沒有覆蓋。
移動距離計算
假設房間1 和房間2發生碰撞,
對於房間1 Offset = pos1-pos2 如果選擇豎直方向的移動 需要判斷是向上還是向下 方向:dirY = offsetY>0?1:-1; 距離:dis = y1/2+y2/2 – abs(offsetY) pos1 += vector2.up*dirY*dis 水平方向移動同理。
然后在FixUpdate中調用Simualeting。
篩選主房間
這里沒什么好說的了,就是這個篩選房間可以在碰撞之前進行。
還有就是 item.size.Sum();這個是自己寫的一個方法。
三角剖分
這里是第一個難點,花了不少時間理解和實現。
三角剖分就是將多個點連起來生成多個三角形,並且三角形之間沒有重疊的部分,或者說是線段沒有交叉。
就是類似下面的圖:
這里實現的是Delaunay三角剖分算法,
我先走一遍流程。
首先生成一個三角形(又稱超級三角形),這個三角形將所有的點都包含進去。
例如:
要實現將所有的點包圍起來,我的想法是:
找到x和y的最大值和最小值,生成一個矩形,利用這個矩形生成一個圓形。
最后利用這個圓形生成一個三角形,這個三角形內接剛才生成的圓形。
如圖:
將這個超級三角形放入待確認的三角形列表中。
開始遍歷三角形列表,
如果當前三角形的外接圓內有點,
選定一個點,將當前三角形拆分成三個邊,讓這個點和三個邊分別連接成三角形,
將這三個三角形放入列表中,並且將當前三角形刪除。
如果三角形的外接圓內沒有點,那么這個三角形就是Delaunay三角形。
將三角形放入Delaunay三角形列表中。
上面的流程看起來挺正確的,
但似乎忽略了一種情況
按照剛才的算法,會生成下面的情況:
這時候應該怎么辦?
不急,先執行另一個三角形看看
執行后多出來了三個三角形,這里用了藍色的線標記起來。
此時有兩個三角形是完全相同,(大小相同,位置相同)
如果將這兩個三角形刪除,就會得到下面的情況。
這可太美妙了。(當時理解的時候的想法)
下面展示一下偽代碼
輸入頂點列表 對頂點的x進行排序 生成一個超級三角形,並添加到待確定三角形列表中 遍歷頂點 遍歷待確定三角形列表 如果該點在三角形外接圓中 將三角形拆分,分成三個邊,將三個邊添加到緩存區中,將本三角形刪除 繼續當前循環 如果該點在三角形外接圓的右側,即該點的x>三角形外接圓最右側 這時候所有的點都不可能在圓內, 此時將三角形添加到Delaunay三角形列表中 將三角形從待確定三角形中移除。 如果該點不在三角形外接圓右側,這時候不能確定其他點不在三角形內部 繼續當前循環。 查找緩存區中相同的邊, 將相同的邊根除,即一條邊不止出現一次,就將這條邊在列表中全部刪除。 將頂點和緩存中所有的邊組合成三角形。 添加剩下的三角形到delaunay三角形列表中。 算法完成 刪除與超級三角形相關的三角形。 輸出Delaunay三角形列表。
代碼中使用了排序,外接圓右側是個什么意思
其實是這樣的
如果沒有點1 和點2 ,這時只有點3和點4 ,遍歷順序是3 4,
判斷點3 在圓外時,點4一定也在圓外。
但是如果點1 2 都存在,那么遍歷順序是1 2 3 4
此時點1 在圓外,而點2在卻圓內。
這樣懂了吧。
至此三角剖分環節結束了,
代碼參考(代碼是真的僅供參考,因為代碼能力比較emmmm)
生成超級三角形
對頂點列表進行排序
遍歷頂點列表
遍歷三角形列表
如果頂點在三角形外接圓右側
如果頂點在三角形外部,而不是在右側
如果頂點在外接圓內部,拆分三角形,將三角形添加緩存區,
如果已經存在緩存區中,添加到待刪除的緩存區。
根除重復的邊(上面講的是根除三角形,其實同理)
生成新的三角形,添加到待確認的列表中
最后合並所有的三角形,然后刪除超級三角形相關的三角形。
要是覺得講的不好可以去看一些我當時參考的博客
https://www.cnblogs.com/zhiyishou/p/4430017.html
https://blog.csdn.net/keneyr/article/details/90316467
最小生成樹算法
從上面獲取的三角形列表,將三角形列表轉換成邊列表,對邊列表進行最小生成樹算法,
這里使用的是Kruskal的最小生成樹算法
我就直接把算法流程畫出來,看的方便
我們先從最小的邊開始連接,這時候看邊的兩端的父親是否為同一個節點。
如果沒有父親,那自己就是父親節點。
接下來是第二條邊,同樣因為父親節點不相同,所以連接起來
接下來邊是1-3這條邊,但是節點1和節點3 的父親是相同,所以不能連接起來。
然后選擇2-4這條邊,連接起來。
剩下的同理
最小生成樹有個性質,樹的邊數量等於節點數量-1
所以終止的條件是樹的邊數量等於節點數量-1;
下面是代碼:
首先對邊的權重進行排序
對邊進行遍歷
判斷邊的兩個節點的父親是否是同一個節點
至此最小生成樹就結束了
最后我們要隨機幾條邊到生成的路線中。
這里的方法比較簡陋,就是隨機找幾個,然后添加進去。
現在大部分工作已經准備好了,剩下的就是如何講我們的房間和道路展示出來
繪制房間和道路
這里采用了Unity中Tilemap工具來繪制房間和道路。
按照文章的設想,房間之間的通路是由水平和垂直的路組成的,並不是斜着的路。
如圖
但是兩個房間的路又存在兩條,就像moba類游戲中的上路和下路一樣,我們應該選擇哪一種
如圖:
雖然很不情願,但是我還是得使用隨機來指定路線,
接下來又面臨一個問題,房間的出口究竟應該設置在哪里?
如果是中間,這樣看起來就不夠多樣性。那只能選擇隨機了。沒錯又是隨機,畢竟是隨機生成地牢嘛。
我的想法是在圖中紅色的區域隨機生成一個點,作為兩個房間道路的拐角點或者是轉折點。
代碼:
為了方便以后放置門,我們還需要講房間的出口計算出來
看圖可以看出來房間A的出口坐標
DoorA.x = room.x+size.x/2
DoorA.y = Turnpoint.y
看上去很完美,但是如果出現了下面的情況門口就會出現問題。
假設拐角點在房間內,按照上面的計算公式,就會出現下面的情況
為了排除這種情況,有兩種做法,
一、禁止轉折點在房間內生成,這個也有問題,我不過多解釋。
二、刪除房間B的出口,將轉折點修改為真正的出口
我的選擇自然是刪除房間B的出口
看圖可以看出轉折點的位置是
Turnpoint.x = Turnpoint.x
Turnpoint.y = roomb.y+size.y/2;
再將兩點返回
最后調整一下每個方向應該的值,微調一下代碼(這個微調花了我最多時間)
代碼參考
這里稍微解釋一下,代碼中有很多這樣的語句
(dir.x > 0) ? offset - 1 : -offset) 或者 (dir > 0) ? (roomB.size.y / 2) - 1 : -(roomB.size.y / 2)
這些 - 1 都是為了能夠在Tilemap中繪制圖的時候不會出錯
如果我不減1 就會出現
心目中的6*6
現實中畫出來的6*6 注意:這里指的是如果不 減 1 的情況。
接下來繼續上代碼
將主房間和次方間,以及道路繪制出來
繪制房間到Tilemap的代碼:
繪制道路到Tilemap代碼
最后生成效果展示
終於結束了,真的花費了太長的時間,感覺有些不務正業hhh,
當然問題還是有的,比如,大量的射線檢測導致的性能問題,還有道路的選擇也有寫小問題。
由於房間之間的碰撞使用的是組件,所以碰撞的順序常常不相同,即使設置隨機種子,房間的位置也會不相同。
剩下的以后有機會再寫,保證不咕咕咕。
最后,我寫了一個月才完成了這些代碼,不知道大佬們需要花多久。
參考文章:
隨機地牢作者的博客:
https://www.gamasutra.com/blogs/AAdonaac/20150903/252889/Procedural_Dungeon_Generation_Algorithm.php
隨機地牢作者的github:
https://github.com/adnzzzzZ/blog/issues/7
隨機地牢的國內翻譯:
http://www.gamelook.com.cn/2015/12/239245
圓內隨機點算法:
https://stackoverflow.com/questions/5837572/generate-a-random-point-within-a-circle-uniformly
三角剖分算法:
https://www.cnblogs.com/zhiyishou/p/4430017.html
https://blog.csdn.net/keneyr/article/details/90316467
最小生成樹算法:
https://blog.csdn.net/qq_41754350/article/details/81460643
其他生成算法(未參考):
https://indienova.com/indie-game-development/roguelike-dungeon-building-algorithm/
https://indienova.com/indie-game-development/rooms-and-mazes-a-procedural-dungeon-generator/