技術貼:如何簡單地做游戲隨機生成地圖


轉自:http://www.gamelook.com.cn/2015/12/239245

Gamelook報道/對於大多數的游戲來說,內容的消耗都是開發商非常棘手的問題,而隨機生成地圖的做法則大大增加了游戲的可重復性,並且可以豐富玩家的體驗。最近,海外一名資深開發者在博客中分享了他做隨機生成地圖的方式,以下請看Gamelook編譯的博客內容:

這篇博客主要解釋的是一個做隨機生成地圖的技術,之前TinyKeepDev也進行過簡略的描述,但我這里會用更多的細節和步驟來解釋這個做法,總體來說,整個算法的運行方式可以用下面的gif圖表示:

m1

生成房間

首先,你要生成一些寬和高不同的房間,隨機地放在一個圈內。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

你可以在這個鏈接里獲得更多的信息(英文版),在此之后,你就應該可以做出像下圖這樣的東西了:

m2

你需要考慮的一個非常重要的事情是,由於你(至少是概念上)在處理一個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里我是用平常的速度運行模擬,但當你們做不同關卡之間的模擬時,可以用更快的速度。

m3

這些物理物體本身並沒有和tile網格相聯系,但當設定了房間位置並且和隨即指令放到一起的時候,你就會得到這些並不重疊的房間,而且這些房間與tile網格是匹配的,下面的gif對此進行了展示,藍色外形是物理物體,在它們和房間之間總有一些不匹配,因為他們的位置始終是分散的。

m4

當你希望創造水平或者垂直分布的房間時,這樣的做法可能會出現一個問題,比如我現在在做的游戲:

m5

 

游戲里的戰斗都是水平向的,所以我的游戲當中大多數的房間都更更寬,但可能沒有那么高。問題在於,物理引擎如何解決這些比較長的房間在一起的時候出現的沖突:

m7

你們可以看到的是,地牢變得非常高,這並不是理想中的狀況。為了解決這個問題,我們可以一開始就把這些房間按帶狀分布而不是環形,這可以確保地牢本身有合適的寬高比例:

m8

為了在這個帶狀區域里隨機分布,我們只要把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的房間都會被選擇。

m9

三角剖分(Delaunay Triangulation)+圖形

現在,我們把所有選中房間的中間點找出來然后放到剖分程序中,你可以自己做這個過程,也可以找有經驗的人分享這方面的資源。我在做游戲的時候比較幸運的是Yonaba已經做了這個過程。你們可以在界面中看到:

m10

在有了這些三角形之后,你隨后就可以生成一個圖形,這個過程可以非常簡單地給你帶來圖形信息數據結構或者數據庫。如果你沒有做過,那么房間物體或者結構最好有獨特的ID,這樣你就可以把這些Id加到圖形中,而不是來回復制。

最小化生成樹(Spanning Tree)

在此之后,我們從圖形中生成了一個最小化的生成樹,需要再強調一次的是,你可以自己做也可以找有經驗的人去做(前提是和你使用的同一種編程語言)。

m11

最小化生成樹可以確保地牢中所有的主房間都是可達的,但同樣將讓這些房間的連接方式和此前不一樣。這是很有用的,因為我們通常都不希望做一個連接太緊密的地牢,但也不希望做成不可達的孤島。然而,我們又不希望地牢只是一個平行的路徑,所以我們現在要做的就是為剖分圖形增加一些邊界:

m12

這可以增加更多的路徑和循環,這會讓副本地牢變得更加有趣。TKdev當時是增加了15%的邊界,而我發現8-10%是更好的選擇,當然,這主要取決於你希望這些副本地牢之間的連接密度是怎樣的。

走廊

最后,我們希望為地牢增加走廊,為此,我們檢查了圖形中的所有節點,然后在相鄰的節點之間我們可以創造直線,如果這些相鄰節點排列比較平行的話,我們就可以做一個水平線。如果這些節點比較垂直,我們可以做垂直線,如果這些節點沒有相鄰也沒有平行或者垂直,我們可以做2跳線形成L形狀。

我測試是否相鄰的標准是,計算兩個節點之間的中間點,然后檢查中間點X或者Y的屬性是否在節點的邊界之內,如果在,我就可以從這個中間點創造這條線,但只能在一個軸上。

6

在上圖中,你們可以看到所有情況下的例子,節點62和47之間有一個平行線,60和125之間有一個垂直線,而118和119之間有一個L形線。另一個比較重要的是,這些都不是我創造的線,這些只是我正在畫的,但我還在每個線的旁邊創造了2個額外的線,確保每一個都能夠與tile尺寸匹配,因為我希望游戲中的戰士寬度和高度都至少達到3個tiles。

不管怎么說,在這個過程之后,我們可以檢查哪些並非主房間的房子與這些線沖突,有沖突的房間可以被加到任何你在用的結構中,而且它們還可以作為走廊的輪廓:

根據你最初設定房間的尺寸和均勻度,你到這里就可以獲得外觀不同的副本地牢了,如果你希望讓走廊變得更統一而且看起來不那么奇怪,那么你就應該把偏差做小一些,而且應該做一些檢查,確保房間不至於太窄或者太寬。

7

作為最后一步,我們只需要增加1個tile尺寸的網格房間不缺漏掉的部分即可,需要說的是,你其實並不需要有網格數據結構或者太花哨的東西,你可以根據tile尺寸檢查每條線,並且在某些列表中增加網格分布位置即可,這就是我們增加3條線(或者更多)的原因。

8

m13

接下來,我們的隨機生成地圖就完成了。

總結

m14

整個流程中我返回的數據結構是:一個房間列表(每個房間都只是帶有獨特ID的結構、x/y位置和寬高比);圖形,每個節點對應一個房間id;真實的2D網格,這里的每個房間都是空的,可以指向主房間、走廊或者走廊間。有了這三個結構,我認為你可以做出任何類型的數據,然后找到在哪兒放門、敵人、物品,決定哪些房間里有BOSS等等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM