數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

猴與花果山發表於2020-07-06
對於Roguelike類遊戲而言,隨機地圖是一個非常核心的元素,而在很多Diablolike遊戲中,隨機地圖也依然表現得非常活躍。我們可能看到過很多隨機地圖的生成演算法,包括且不限於GDC以及GMTK等知名的遊戲交流媒體的分享。但是絕大多數隨機演算法都會要求手工預設關卡的大多內容,包括Diablo等著名的遊戲在內,都是預先拼好了一些地圖,然後只是在隨機位置隨機挑選了這些地圖中的幾個用上。畢竟這樣做的好處是:地圖看起來是美的,而且不會有死圖(即發生入口無法通向出口的情況),並且刷怪位置相對合理。

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖
(隨機地圖生成演算法,是Roguelike遊戲的重要課題)

那麼有沒有一種不需要手動預設內容的隨機演算法,只需要調整一些引數,它就可以生成出看起來美觀,也不會產生死圖,還能確保生成的刷怪點位置合理的方法呢?今天我們就將深入地探索一下一個尋路演算法——Dijkstra演算法,只要稍微調整一下使用的“手法”,這個奇妙的、被用於尋找最短路徑演算法就會成為一個極其強大的隨機地圖生成演算法,並且對精通於設計數學函式的數值策劃來說,可以輕鬆地用幾個引數就玩轉這個“Dijkstra隨機地圖生成法”。

01 深度解讀Dijkstra演算法

我們通常瞭解的Dijkstra是一個尋找2點之間最短路徑的演算法,這個演算法往往與他的兩個特殊形態Floyd演算法和AStar演算法齊名。

Dijkstra演算法本身非常好理解,即在地圖中的某個點作為起點,然後向四面八方展開尋找,每進入一個格子都累加一個Cost值,然後對於地圖上每一個格子都算出走進這個格子的最短路徑,即Cost值最小的從起點點通向通向這個格子路線,最終遍歷完所有的格子之後,獲得最短的路線。如圖所示:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖
(五角星是起點,叉是終點,格子裡的數字就是進入格子的最短路徑的Cost)

大多情況下,Dijkstra的開銷都很大,所以在Greedy Best-First的思路結合之下,產生了AStar尋路。但是Dijkstra並沒有因為AStar的出現就退出遊戲開發的舞臺,他依然在戰棋類SLG中活躍:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖
(圖為《火紋》的角色可移動範圍顯示)

在戰棋SLG中,角色的移動範圍可以通過Dijkstra演算法得出,畢竟每一個角色的行動力是有限的,並且地圖中的地形會影響行動力,同時敵方角色的臨近格子行動力消耗也會進一步提高,所以Dijkstra演算法被巧妙地用於“不尋找目標點,而是尋找到行動力足以達到的點”。

到這裡是最基礎的Dijkstra演算法的介紹,接著我們需要更進一步的去思考一些問題。我們通常使用Dijkstra演算法的時候,檢查單元格的規則都是“周圍的格子”,但是假如有些格子是一個傳送門入口,走進去以後可以從n個出口中選擇一個,這時候尋路演算法需要做出什麼改變呢?如圖所示:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

如果綠色是起點,紅色是終點,通常我們的尋路法就會產生出一條黑線的路線,但是當我們為地圖上增加了傳送門入口(藍色圓圈),走進這個傳送門,就可以從3個橙色圓圈的傳送門出口中任意選擇一個作為出口的時候,問題就出現了,因為藍色的格子一下子代表了3個其他格子。因此我們可以發現,事實上我們只把Dijkstra演算法用於Tilebased遊戲尋路的時候,理所應當的覺得“周圍的格子”作為“下一個格子”是這個演算法的一部分,但實際上,對於Dijkstra演算法而言,如何選取“下一個格子”,是需要設計的一部分。

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

如上圖所示,Dijkstra演算法的“下一個格子”其實是“下一個node”,也就是根據一個規則加入到判斷列表裡的,如果是傳送門他應該是這樣的:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

由於走進B相當於走進B1或B2或B3,所以我們打斷了B這個“不存在的格子”,連結了B1\B2\B3。這正是Dijkstra演算法的核心之一——“路徑的節點規則”。

02 隨機地圖的準備工作

在瞭解了Dijkstra演算法的性質之後,我們就要開始準備巧妙地使用它來生成隨機地圖了。但是在這之前,我們還需要一些步驟,這些隨機地圖生成的步驟其實是不需要Dijkstra演算法的。

在這裡我們需要數值策劃設計的第一個內容是:地圖資訊=f(迷宮,層數),這個函式的本意也就是根據地圖的難度係數,來算出當前地圖的一些資訊,這些資訊至少要包含:

  • 房間的個數:我們知道大多roguelike地圖是需要房間的,房間個數越多,則迷宮對於玩家來說難度越大。因此對於我們生成地圖來說,房間的個數也是一個核心資料,不知道房間個數就沒法生成難度合適的隨機地圖了。
  • 每個房間的最大、最小尺寸:這個尺寸的寬度高度是可以不同的,但是我們最好認為他們是相等的,這樣可以讓房間的位置相對更加均勻一些。而最小尺寸應該是小於最大尺寸的,如果相等一樣會影響一些隨機的效果。
  • 房間之間的連線最小單元格數:房間之間還是需要有路線連線的,路線的最小長度是可以要求的,這應該是個自然數,如果是0,則可能生成出牆壁緊挨著的2個房間,會影響美觀,當然這也可以在生成之後進行補正,比如消除一堵牆壁等。
  • 房間之間的連線最大單元格數:這應該是一個大於最小tile數的存在,這樣才有了隨機的餘地。
  • 房間怪物數資訊:這個資訊將被用於生成怪物的重新整理點。
  • 最小經過房間數:我們知道Roguelike地圖可能會有這樣的需求,就是我到達這一層之後,至少要走過多少個房間才能遇到去一下層的入口。如果這個值是0,則有可能來到這一層的第一個房間就有下樓的樓梯;當然這個數字肯定是不能大於等於房間數的,不然就走不通了,建議是取房間數的一半。


當以上地圖資料得出之後,我們就可以確定出本層地圖橫向縱向需要的“塊”數,所謂的“塊”即每個隨機房間出現的範圍。為了確保地圖儘可能接近正方形範圍(這僅僅是為了“美觀”),可以這樣:橫向的塊數=(“房間的個數”的平方根)向上取整,縱向的塊數=(總房間數/橫向塊數)向上取整。也就是說,比如是7個房間的地圖,在這裡會被劃分為3x3個塊,如圖所示:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

我們可以很簡單的算出一個“塊”的單元格數:橫向格數=房間最大橫向格數+橫向連線最大tile數;縱向格數=房間最大縱向格數+縱向連線最大tile數。而每個塊的格數乘以塊數,就能算出地圖橫向和縱向總共有多少個單元格。

當確定完單元格之後,我們要解決一個問題就是塊數>房間數的問題,如果是等於,就沒有什麼問題,但是如果是大於,我們就首先要隨機取出(總塊數-房間數)行(或者列),然後在這些行(或者列)中取出1個“塊”刪除掉,如圖所示(圖中取行):

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

我們隨機了第1行和第3行,各去掉一個“塊”。這樣房間數正好是2+3+2=7個。然後我們對於每一行進行隨機分塊,這個分塊的過程,確定了每一個“塊”擁有的橫向、縱向單元格數量,這個最小單元格數應該不小於房間的最小寬度(高度)+橫向(縱向)最短連線單元格數。均攤之後的塊很可能是這樣的:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

從上圖我們可以看出,切塊之後,“塊”與“塊”之間的連線圖也產生了(上圖中只列出了1號“塊”的連線關係),而每個“塊”中僅有1個房間,所以“塊”的連線關係,也是房間的“初步連線關係”,之所以是初步的,因為我們後期還會打斷一些連線。

確定完連線之後,我們就要隨機找出每一個“塊”中的一個隨機點,作為這個“塊”的房間的“中心點”,這個中心點的範圍,至少得符合房間最終能在“塊”內,且儘可能符合最長最短連結單元格的要求。到這裡每一個“塊”的屬性、也就是每個房間的基礎資料就產生了,這個資料中包含了:

  • 房間id:這個房間的id
  • “中心點”座標:這是用於生成房間的核心資料。
  • 連結房間陣列:這個房間可以連線到的房間的id陣列。


到此,我們的準備工作也就完成了,接下來就要開始生成房間的重要演算法了。首先我們回顧一下,一個數值策劃要在這裡具體做一些什麼樣的工作呢?

根據迷宮和層數算出地圖資訊:這是用於隨機地圖生成的最基本資料。
切塊演算法:儘管在本文裡舉了一個例子,但是這並不是惟一的例子,我們當然可以用其他數學方法來確定切塊的方式和房間連線的方式,總之最後可以獲得每個“塊”的資料就行了。

03 妙用Dijkstra生成“房間”

當我們完成了每一個“塊”的資料之後,就可以遍歷每個“塊”,對每個塊進行房間的生成了。首先我們要拿出:

房間的中心點,這是我們用Dijkstra演算法的“尋路起點”。

房間的尺寸:取寬度、高度中較大的一個,然後將其除以2後向下取整,作為一個Cost值,賦值給起點。

有了這兩個資訊之後,我們開始“取周圍8個格子作為下一格”的規則,作為Dijkstra演算法的“下一格”規則。和戰棋類SLG搜尋移動範圍差不多,而唯一的不同則是,這個單元格的Cost消耗量是隨機的,而這個隨機並不是無腦的隨機1-2,他應該有一個演算法,比如越接近起點的,越不容易是2,這是最基礎的規則,如圖所示,是這樣“尋路”的:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

Cost=f(...)就是這個隨機演算法,依據是這個房間(“塊”)的資訊,當然也可以傳入更多的需要的資訊,這取決於遊戲的設計。

之所以要用Dijkstra演算法去生成房間,基礎的原因有2個:

其一是房間最終不一定是矩形的,當然如果cost都是1,那麼最後是一個正方形的,這是特殊情況,通常我們還是希望房間不是矩形的,所以想需要利用Cost的演算法配合Dijkstra演算法產生出“非矩形”的房間,如圖所示:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

把cost=0(紅色)的格子當做牆壁,就生成了房間。當然我們只要調整一下這個Cost=f(...)的函式,就可以發生有趣的變化:

比如y方向上更容易抽到高cost,就可以讓地圖更接近扁的:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

再比如當格子的y值大於起點y值(下方格子),則cost=初始cost:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

可見,只要調整Cost=f(...),就能讓地圖的形狀接近設計的預期效果,而這裡還會追加一個問題,房間內的阻擋和刷怪點是如何產生的。事實上我們已經有了從中心展開的規則,那麼刷怪點和阻擋完全是可以符合:IsPoint = f(x, y, cost)這樣一個數學函式的,即是否這個點刷怪或者阻擋,由引數x,y和這個格子的剩餘cost值來決定的。比如“所有Cost=3的格子中抽取隨機3個作為刷怪點”,這些刷怪點就會接近於中間,這樣玩家走進房間,不論是從哪個方向走過來的,都不至於遭遇“被怪貼臉”的情況(當然也並不是所有遊戲第一時間都需要刷怪的)。

到此,一個房間的生成就完成了,我們從這個生成過程中得出了:

  • 房間的形狀:整個房間佔了那些格子,其中哪些是阻擋單元格。
  • 刷怪點:房間裡的刷怪單元格,至於上面刷什麼怪,不是房間說了算的,是由樓層資訊、難度等算出刷什麼怪,這裡只是提供給怪物他們的出生位置。


在這步,數值策劃的核心工作就是:

  • 每個格子的Cost演算法,即Cost=f(...)的函式,包括其引數以及計算過程。
  • 刷怪點、阻擋的演算法,根據每個格子的情況,算出當前格子是阻擋還是刷怪點。
  • 只要演算法設計得好,就可以用來生成任何玩法的Roguelike遊戲的地圖,甚至用在橫版動作遊戲的地圖生成也不是問題。


04 妙用Dijkstra生成“路線”

當房間生成完之後,我們可以用AStar來生成2個房間之間的連線單元格,這是非常簡單的一件事情,但是在此之前,我們還有一個工作。還記得開始的時候說的“最小經過房間數”嗎?滿足這個需求的方式,是把房間之間的一些連線關係打斷,以確保路線能達標。

首先我們要將房間(或者原本的“塊”)之間的連線確定,製作成“地圖”:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

然後我們通過這個地圖,得出隨機的起點和終點,如果“最小經過房間數>0”,則代表起點和終點必須是不同的2個格子,假設“最小經過房間數”=3,起點為3,終點為6,那麼我們就必須打斷一些連線:

數值策劃如何玩轉Dijkstra演算法來設計隨機地圖

我們需要打斷的是,讓起點到終點距離會<3的關鍵連結。在打斷了關鍵連結之後,我們可以在不關鍵的位置也打斷幾根,這個打斷的依據依然可以是一個數值策劃設計的演算法。

總結

到此,一層樓的隨機地圖就算是產生完了,房間、怪物、阻擋、寶箱等等都可以通過Dijkstra演算法的妙用來算出,而這一切的關鍵,在於數值策劃對於一些數學函式的設計。

當深入瞭解一個演算法的實際工作原理之後,思考並修改他的用法,從而做到一些“非大眾化”的用途,往往在遊戲設計中是可以起到奇效的,關鍵看設計師有沒有能力運用好——“沒有低等的法術,只有低等的法師”。

文章來自“千猴馬的遊戲設計之道”(ID:baima21th) 作者:猴與花果山


相關文章