前言
最近跟團(tuán)隊想要開發(fā)一個開放世界的游戲,這是很有趣的游戲概念,然而參考了《塞爾達(dá)傳說 荒野之息》的設(shè)定后發(fā)現(xiàn),這個游戲的成功很大程度是美工和設(shè)計大量的工作,才形成了這個很有趣的大陸,然而我們的團(tuán)隊沒有辦法手工搭建出這么大的場景,于是我萌生了一個很自然的想法:寫一個自動生成還算合理的地形的腳本。
起初我嘗試了Perlin噪聲直接生成高度圖的做法,然而產(chǎn)生的地形太過極端,腳本的開發(fā)也進(jìn)入了僵局,直到我看到了一篇2010年的文章:
原文地址:
http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation
這篇文章是在flash上實現(xiàn)了一個自動隨機多邊形地圖的demo,我跟著這篇文章的思路,基本上復(fù)制了一個unity3D的版本,以后可能會再寫出一個unreal的版本。
效果圖
基礎(chǔ)多邊形細(xì)胞圖
既然要產(chǎn)生一個多邊形為基礎(chǔ)的地圖,那么第一步當(dāng)然是產(chǎn)生一個由多邊形填滿的二維圖,術(shù)語上這種圖形叫做維諾圖(Voronoi Diagram),我更喜歡叫他細(xì)胞圖。
這種圖形的生成原理網(wǎng)上有很多,我參考了這篇文章:
http://blog.csdn.net/k346k346/article/details/52244123
需要注意的是,采用這種方法生成出的圖形太過隨機,我們需要讓他看起來齊整一些,這采用了叫做Lloyd的放松算法,這是一種聚類算法,簡單來講就是經(jīng)過數(shù)次迭代,每次迭代將維諾圖的中心點移動到所在多邊形的中心,重新計算維諾圖。實際實驗后發(fā)現(xiàn)2次迭代已經(jīng)完全能達(dá)到效果。
數(shù)據(jù)結(jié)構(gòu)
源代碼中的VoronoiElement文件里保存了數(shù)據(jù)結(jié)構(gòu),大部分都具有注釋,需要解釋的Center類代表了維諾圖的中心點,也就是Delaunay三角網(wǎng)的頂點,而Corner類代表三角形的外心,也就是圖中多邊形的交點。這樣做的目的事實上產(chǎn)生了兩張圖的信息,三角圖 和多邊形圖,后期開發(fā)時三角圖可以用作尋路而多邊形圖主要用于渲染。
區(qū)分陸地和水
第二步我們要為我們的地圖添加大陸和水的區(qū)別,自然界中的地表水的形成是有很多因素的,然而我們模擬時只能采用隨機模擬。
原文中給出了很多模擬的思路,我才用了柏林噪聲的方式進(jìn)行模擬,首先為地圖上每一個Corner點計算一個Perlin噪聲值,設(shè)定一定的參數(shù)決定這個噪聲值是否能使這個Corner具有水的屬性。我才用的方式是比較(PerlinNoise)與(waterScale + waterScale * Distance(地圖中心,該點))大小,這樣既可以修改waterScale的參數(shù)值來修改生成地圖的水量多少,同時也確保在地圖邊緣更容易出現(xiàn)水,讓我們的地圖看起來像是一個海上的大陸(我默認(rèn)地圖邊緣都是水)。
在決定好Corner點的屬性后,我們把與水Corner相接的Center(也就是多邊形)記為水屬性,可以通過Corner的touches鏈表獲取到與其相接的Center。
區(qū)分海洋、內(nèi)陸湖、海岸線
首先我們規(guī)定與地圖邊界接壤的多邊形為海洋,所有與海洋接觸的水均為海洋,與海洋接觸的陸地是海岸線,其余的水為湖泊。
這里采用了一種種子填充的算法來計算。實際上這是一種廣度優(yōu)先搜素的策略,我們首先將地圖邊緣的Center添加到一個Queue隊列中,接下來執(zhí)行一個循環(huán),直到隊列中沒有剩余元素。循環(huán)時每次取出隊列的尾部Center,檢查與該Center直接接觸的臨近多邊形(這可以在neighbors鏈表中找到),若臨近Center為水且不為海洋,代表這是一個還未處理過的Center,將他置為海洋并加入隊列,若臨近Center不為水,則置為Coast并加入隊列。
在處理完全部的Center后,我們只需把海洋多邊形的Corner置為海洋,海岸線多邊形的Corner置為海岸線即可。
賦予高度
接下來我們?yōu)槲覀兊牡貓D添加高度的元素,讓我們的地形變得崎嶇。
為了地形隨機但是合理,我們采用了這樣一種策略:
靠近海岸線的地方更容易地勢低,島嶼中心的地方更容易地勢高,這樣是很合理的。
實現(xiàn)方案同樣是廣度優(yōu)先搜索,我們?yōu)槊恳粋€Corener定義了一個elevation的屬性,代表距離海岸線的最短距離,每經(jīng)過一個Corner,這個距離就加上一定的隨機值(這個隨機值范圍越大地形會變得越崎嶇)。
在搜索結(jié)束后,我們把每個Corner的elevation統(tǒng)一到0~1,作為這個點的高度,而每個Center的高度為多邊形所有corner高度的平均值。
需要注意的是,我們在統(tǒng)一Corner高度后需要進(jìn)行一個額外的操作,我們要計算每個Corner下降梯度最大的方向,這會為以后的河流生成做準(zhǔn)備。
河流
在上一步時我們已經(jīng)計算好每個Corner的下降梯度最大的方向,這樣我們的河流生成就簡單了,我們只需要給出幾個參數(shù):河流的最小生成高度,河流的數(shù)量范圍。然后我們隨機給出河流的起點(確保大于最小高度,以及在陸地上),然后讓河流按梯度向下流淌,流過的每個Corener置為河流,直到流入湖泊或海洋,或者經(jīng)過一定路程
- 到這里我們的地圖已經(jīng)很好了,但是為了游戲性我們?nèi)匀粸槎噙呅翁砑觾蓚€特征
濕度和生物群落
我們?nèi)匀徊捎昧撕妥匀唤缦喾吹倪^程,我們通過多邊形到水源的距離來決定多邊形的濕度,計算的方案和高度計算基本一致。
有了濕度和高度后我們就可以模擬地形上的植物類型了,這里采用了這樣一張圖表來進(jìn)行模擬:
讀者可以自行修改數(shù)據(jù)讓地形變得更干燥或者濕潤
渲染
到目前為止我們都是用圖形化調(diào)試的方式展示,我們還需要渲染出3D網(wǎng)格。
我采用了Unity自帶的Mesh渲染的方式,為一個GameObject添加一個MeshRenderer和MeshFilter組件,修改mesh的頂點以及三角形屬性,把所有的Center和Corner都作為頂點,對于每個多邊形,讓Center頂點和每一條邊組成一個三角形網(wǎng)格(這里注意三角形要按順時針排序頂點,確保法線方向正確),同時為mesh分13個submesh(代表13種群落),根據(jù)多邊形的生物群落決定將三角形網(wǎng)格分配給哪個submesh(mesh.SetTriangles),為MeshRenderer分配13個材質(zhì),完成渲染。
尾聲
目前不同群落的材質(zhì)我只用純色替代,以后會精致制作每一個材質(zhì),實現(xiàn)類似低多邊形渲染的效果。
且有很多噪聲、融合的計算還沒有做,如混合邊界等。
文中步驟的圖片都是一共500個多邊形,而最后的效果圖共有2000個多邊形,從編譯、啟動、計算到渲染完成一共耗時20s左右,實際游戲運行中計算耗時會更低,且完全沒有用到unity引擎的庫所以可以放在多線程中完成。