轉自:http://www.gamelook.com.cn/2015/12/239245
Gamelook報道/對於大多數的游戲來說,內容的消耗都是開發商非常棘手的問題,而隨機生成地圖的做法則大大增加了游戲的可重復性,並且可以豐富玩家的體驗。最近,海外一名資深開發者在博客中分享了他做隨機生成地圖的方式,以下請看Gamelook編譯的博客內容:
這篇博客主要解釋的是一個做隨機生成地圖的技術,之前TinyKeepDev也進行過簡略的描述,但我這里會用更多的細節和步驟來解釋這個做法,總體來說,整個算法的運行方式可以用下面的gif圖表示:
生成房間
首先,你要生成一些寬和高不同的房間,隨機地放在一個圈內。TKdev的算法用了比較常見的方法隨機生成房間尺寸,我認為這是一個不錯的想法,因為它可以為你帶來更多的參數可供使用,使用不同的寬高比例和標准偏差可以帶來外觀不同的副本地牢。
你可能需要使用到的一個函數就是getRandomPointInCircle:
function getRandomPointInCircle(radius) local t = 2*math.pi*math.random() local u = math.random()+math.random() local r = nil if u > 1 then r = 2-u else r = u end return radius*r*math.cos(t), radius*r*math.sin(t) end
你可以在這個鏈接里獲得更多的信息(英文版),在此之后,你就應該可以做出像下圖這樣的東西了:
你需要考慮的一個非常重要的事情是,由於你(至少是概念上)在處理一個tile網格,所以你必須把所有的東西都對齊到同一個網格里,在上圖的gif中tile的尺寸是4像素,意味着所有的房間位置和尺寸都必須是4的公倍數。為了做到這樣,我把位置和寬高比例都放到了一個函數中,把這些數字和tile尺寸相匹配:
function roundm(n, m)
return math.floor(((n + m – 1)/m))*m
end
— Now we can change the returned value from getRandomPointInCircle to:
function getRandomPointInCircle(radius) … return roundm(radius*r*math.cos(t), tile_size),end
分散的房間
現在,我們可以說說分離的部分了。有很多的房間都混在了同一個地方,而且它們之間不能有重疊。TKdev使用了分離轉向的做法,但我發現用一個物理引擎做起來更簡單。在你增加了所有的房間之后,只要增加物理物體(solid physics body)匹配每個房間的位置、然后運行模擬,直到所有的物體都出於休眠狀態。在gif里我是用平常的速度運行模擬,但當你們做不同關卡之間的模擬時,可以用更快的速度。
這些物理物體本身並沒有和tile網格相聯系,但當設定了房間位置並且和隨即指令放到一起的時候,你就會得到這些並不重疊的房間,而且這些房間與tile網格是匹配的,下面的gif對此進行了展示,藍色外形是物理物體,在它們和房間之間總有一些不匹配,因為他們的位置始終是分散的。
當你希望創造水平或者垂直分布的房間時,這樣的做法可能會出現一個問題,比如我現在在做的游戲:
游戲里的戰斗都是水平向的,所以我的游戲當中大多數的房間都更更寬,但可能沒有那么高。問題在於,物理引擎如何解決這些比較長的房間在一起的時候出現的沖突:
你們可以看到的是,地牢變得非常高,這並不是理想中的狀況。為了解決這個問題,我們可以一開始就把這些房間按帶狀分布而不是環形,這可以確保地牢本身有合適的寬高比例:
為了在這個帶狀區域里隨機分布,我們只要把getRandomPointInCircle函數進行改變,把分布點放到橢圓形中即可,在gif里我使用的橢圓形寬度為400,高度為20):
function getRandomPointInEllipse(ellipse_width, ellipse_height) local t = 2*math.pi*math.random() local u = math.random()+math.random() local r = nil if u > 1 then r = 2-u else r = u end return roundm(ellipse_width*r*math.cos(t)/2, tile_size), roundm(ellipse_height*r*math.sin(t)/2, tile_size) end
主房間
下一步主要是解決哪些房間是主房間或者中心房間,哪些是附屬房間。TKdev的方法是非常不錯的,你只需要挑選超過一定寬高比閥值的房間即可,在下面的gif里,我用的閥值是1.25,也就是說,如果平均寬和高是24,那么超過寬和高30的房間都會被選擇。
三角剖分(Delaunay Triangulation)+圖形
現在,我們把所有選中房間的中間點找出來然后放到剖分程序中,你可以自己做這個過程,也可以找有經驗的人分享這方面的資源。我在做游戲的時候比較幸運的是Yonaba已經做了這個過程。你們可以在界面中看到:
在有了這些三角形之后,你隨后就可以生成一個圖形,這個過程可以非常簡單地給你帶來圖形信息數據結構或者數據庫。如果你沒有做過,那么房間物體或者結構最好有獨特的ID,這樣你就可以把這些Id加到圖形中,而不是來回復制。
最小化生成樹(Spanning Tree)
在此之后,我們從圖形中生成了一個最小化的生成樹,需要再強調一次的是,你可以自己做也可以找有經驗的人去做(前提是和你使用的同一種編程語言)。
最小化生成樹可以確保地牢中所有的主房間都是可達的,但同樣將讓這些房間的連接方式和此前不一樣。這是很有用的,因為我們通常都不希望做一個連接太緊密的地牢,但也不希望做成不可達的孤島。然而,我們又不希望地牢只是一個平行的路徑,所以我們現在要做的就是為剖分圖形增加一些邊界:
這可以增加更多的路徑和循環,這會讓副本地牢變得更加有趣。TKdev當時是增加了15%的邊界,而我發現8-10%是更好的選擇,當然,這主要取決於你希望這些副本地牢之間的連接密度是怎樣的。
走廊
最后,我們希望為地牢增加走廊,為此,我們檢查了圖形中的所有節點,然后在相鄰的節點之間我們可以創造直線,如果這些相鄰節點排列比較平行的話,我們就可以做一個水平線。如果這些節點比較垂直,我們可以做垂直線,如果這些節點沒有相鄰也沒有平行或者垂直,我們可以做2跳線形成L形狀。
我測試是否相鄰的標准是,計算兩個節點之間的中間點,然后檢查中間點X或者Y的屬性是否在節點的邊界之內,如果在,我就可以從這個中間點創造這條線,但只能在一個軸上。
在上圖中,你們可以看到所有情況下的例子,節點62和47之間有一個平行線,60和125之間有一個垂直線,而118和119之間有一個L形線。另一個比較重要的是,這些都不是我創造的線,這些只是我正在畫的,但我還在每個線的旁邊創造了2個額外的線,確保每一個都能夠與tile尺寸匹配,因為我希望游戲中的戰士寬度和高度都至少達到3個tiles。
不管怎么說,在這個過程之后,我們可以檢查哪些並非主房間的房子與這些線沖突,有沖突的房間可以被加到任何你在用的結構中,而且它們還可以作為走廊的輪廓:
根據你最初設定房間的尺寸和均勻度,你到這里就可以獲得外觀不同的副本地牢了,如果你希望讓走廊變得更統一而且看起來不那么奇怪,那么你就應該把偏差做小一些,而且應該做一些檢查,確保房間不至於太窄或者太寬。
作為最后一步,我們只需要增加1個tile尺寸的網格房間不缺漏掉的部分即可,需要說的是,你其實並不需要有網格數據結構或者太花哨的東西,你可以根據tile尺寸檢查每條線,並且在某些列表中增加網格分布位置即可,這就是我們增加3條線(或者更多)的原因。
接下來,我們的隨機生成地圖就完成了。
總結
整個流程中我返回的數據結構是:一個房間列表(每個房間都只是帶有獨特ID的結構、x/y位置和寬高比);圖形,每個節點對應一個房間id;真實的2D網格,這里的每個房間都是空的,可以指向主房間、走廊或者走廊間。有了這三個結構,我認為你可以做出任何類型的數據,然后找到在哪兒放門、敵人、物品,決定哪些房間里有BOSS等等。