刷怪不再是體力活!設計一個適用於所有ARPG遊戲的刷怪機制

文/猴與花果山

相信做過ARPG遊戲的人,尤其是負責刷怪的策劃,都多多少少會有一個感覺——最後我們遊戲做出來了,怪也刷出來了,但是總覺得哪兒不對勁。這個不對勁,通常包括:一些腦洞的功能最後沒能實現;不論什麼怪物刷出來都像喪屍一樣“沒有腦子”;或者是刷個怪真不容易,海量的數據要填寫。是的,這些感覺都沒錯,因為我們總認為刷怪是個簡單的小功能,從來不重視他,甚至會把一些真實的需求給搪塞了,以至於最後刷怪就被默認為是這麼一件做出來效果不咋的的體力活。

刷怪設計的基本需求

  • 設計背景

假設這是一個Diablo-Like的ARPG遊戲,也就是2.5D的視角,不管渲染是3D的還是2D的,總之它有一個特性——你一眼望去是可以看到地圖的“一小塊區域的”

刷怪不再是體力活!設計一個適用於所有ARPG遊戲的刷怪機制

就像這樣,你的鏡頭可以看到地圖上的一些景物,和部分的怪物。

我們現在正在一張地圖中某個區域(而不是完整的地圖,其實設計的時候你的確也應該去分塊設計,當然這不是本文的重點),假設這個區域是一個山賊的寨子,而你的工作是為這個寨子設計刷怪規則。

  • 設計思考

首先一些常規的思考——會刷什麼兵種在這裡;有多少個刷怪點大約分佈在哪兒;刷的怪等級多少等等的問題,這個毫無疑問。在完成這些基礎元素的思考之後,我們會,也應該去開腦洞想一些問題:

  1. 刷怪的時候,是不是應該有個條件?比如雪天的時候刷的是一批山賊,而大熱天刷的是另外一批?再比如白天刷的山賊數量應該比晚上要多?
  2. 同一個刷怪點上的怪一定只有一種嗎?這個點上是不是在一定條件下刷的是刀斧手,而當另一些情況下刷的是弓箭手?雖說我們可以用一句“隨機”概括掉,但是這樣的條件真的應該存在嗎?
  3. 刷出來的同一種怪應該是完全一樣的嗎?刷在寨子門口作為門衛的長矛兵,和刷在火堆邊用來表現一群山賊在“吹牛逼”的長矛兵,在沒有進入戰鬥(或者說沒有發現敵情的時候),他們的行動應該是一樣的嗎?是不是至少門衛應該是站著不說話的,而火堆邊的應該是坐著話很多的?
  4. 如果我們做的恰好是一個MMO,或者即使只是CO-OP的,也有這樣的問題——是不是玩家多的時候怪應該刷的多點快點?而玩家少的時候怪刷的少點慢點?

想到這裡,是不是發現想法越來越精細了?這是個好事情,當你想得越精細的時候,這個遊戲的品質就越高,先不要被“程序哥哥”的“這做不到”難住了,我們繼續開腦洞:

  1. 是不是火堆邊上只有一個怪的時候他是不會吹牛逼的?畢竟至少得有一個聽眾?同理,是不是2個小兵一組對戳才能練兵?而當中一個還沒刷的時候另外一個是不練兵的?
  2. 是不是一旦下雨了,門衛小兵還得站在那裡,但是圍坐著吹牛逼的小兵就該回帳篷了?或者夜幕來臨的時候,他們還會換崗?

毫無疑問,你還能想到更多的細節,這些細節都做出來,至少刷的怪就不會像喪屍一樣,不管刷在哪兒都是在那裡漫無目的的踱步。但是為什麼我們很少在國遊裡看到這些落實呢?因為背後的代價太大了,要做這個效果,在沒有好的設計的情況下,簡直是天方夜譚。所以我們接下來就要開始提煉需求了。

需求的分析和提煉

  • 所需數據塊

要提煉出整個刷怪功能需要些什麼數據,我們首先還是應該深入的分析一下這個刷怪功能。我們一度把這個問題簡化到:

刷怪不再是體力活!設計一個適用於所有ARPG遊戲的刷怪機制

原本的簡單設計

從這個腦圖可以看出,通常我們認為,一個地圖上有N個刷怪區域(甚至有些遊戲用的是點),每個區域裡面有刷多少個怪(點的話就是1);然後有一個刷什麼怪的List,他的數據是這個怪物的id和這個怪物被刷出來的概率和權重;最後是怪物死後多久刷新,甚至可能這個屬性也會丟到怪物數據裡。

這裡就出現了一個非常嚴重的邏輯錯誤:搞混了怪物表數據(Model)和怪物實體數據(Obj)或者叫怪物的運行時(runtime)數據。相信你有遇到過類似這樣的問題——不同等級的長槍兵就是怪物表裡2條數據;同一個等級的2個長槍兵,因為掉落甚至是AI不同就可以又是2條數據。但是我們仔細想一下,這樣一來,怪物表的作用還對嗎?我相信絕大多數人在最初設計怪物表的時候,他所想做的事情就是把“怪物分類”做一個表出來——所有的長槍兵都是同一條數據,如果還有個弓箭手,他會是第二條數據,但是不管是52級的弓箭手還是87級的弓箭手,他們應該都是一條數據,通過f(等級,怪物model)這個函數,我們可以得出這個怪物在任何等級時候不同的數據。

我們先把這個問題丟在一邊,來看一些更嚴重的問題——目前我們遊戲中有300種怪物,現在到了聖誕節了,我增加了聖誕長槍兵,聖誕弓箭手,他們不同於長槍兵和弓箭手,所以不管你的怪物表怎麼用(即上面說的問題),他們都是新的數據。但是,需求是,我原本刷長槍兵的地方有機率刷(而不全部換成)聖誕長槍兵。這個需求聽起來問題不大吧?非常合理!但是如果我們發現一個地圖裡面有120個點,其中大約30-50個點有刷長槍兵,這時候,第二個問題就暴露出來了——後期維護數據的時候,這是一個幾乎不可能的任務,甚至因為上面說的怪物表作用模糊問題,還會導致因為增加掉落物品,而讓追加刷怪變成日常行為。

因此這樣一個刷怪的數據是錯誤的,不光之前我們腦洞的一些需求不太好加,就連本身維護都非常困難,所以我們要重新思考:

刷怪不再是體力活!設計一個適用於所有ARPG遊戲的刷怪機制

符合需求的設計

可以看出,經過仔細思考,我們刷怪需要的數據其實比我們之前隨意想的要多出很多,這些數據及其主要作用:

  • 當前地圖Runtime數據

即當前服務器(或者單機就是內存裡)上實際在跑的這張地圖的運行時數據,在這個舉例中它包含的信息包括:

  1. 天氣:即我們之前思考的,不同天氣的時候刷怪是不同的,比如夏天刷蚊子,到冬天就刷戰鬥機了,這樣的設計,只要策劃這裡通過了,那就一定是合理的。或者我們還可以更精細的分出當前是雨天、晴天還是什麼等,這些都是邏輯信息,不要荒廢了渲染程序員辛苦做的下雨下雪的效果,他們用的好也很好玩。
  2. 時間:之前我們想的時候也考慮過,當前服務器上這個地圖的時間,下午1點還是早上8點半?每一個地圖都可以是一個“不同的星球”,有自己的時間系統,我們只是依賴這個數據來決定刷怪結果。
  3. 地圖上玩家的信息:這個是一定有的東西,至少我們在這裡也會關心地圖上有多少個玩家在進行遊戲,這也是之前我們腦洞中說過的“玩家多的時候刷怪多且快”這個需求的根本。
  4. 其他數據:根據策劃設計,我們還應該考慮有其他數據在這裡維護,但是最重要的是,我們很多項目裡面可能壓根就沒有給一個正在運行的地圖做這樣一個數據塊,甚至所謂的“地圖”也是根據美術數據來的,而不是依賴於邏輯數據的(即美術做了一張地圖,所以全世界就有了這麼一張地圖,而不是一張地圖可能是有2張策劃設計的地圖,以及服務器運行時候n個副本)。
  • 地圖區域數據

地圖區域數據即當前這張地圖中正在工作的地圖區域的數據,是一個運行時的數據。這個名字其實起的不好,更確切的叫法應該是“怪物刷新組數據”,因為他描述的是地圖中每一個刷怪區域,與實際理解的地圖區域是有偏差的——不同的“怪物刷新組數據”中指向的“區域信息(locationModel)”可能是同一個。

locationModel則是靜態的數據,是策劃事先在地圖上找好的座標組,它的信息非常簡單,通常只需要包括:

  • id(string):這個區域的名稱,被其他一些業務引用。
  • tag(array<string>):這個區域的tag,如有必要可以有tag,被其他業務所引用。
  • area(polygon):當然也可以是rect,這個具體看遊戲設計,是地圖上一個座標區域。

值得注意的是,因為它的功能就是把座標管理起來,而沒有任何其他邏輯作用,所以不要想反了——什麼天氣下激活這個區域跟這個區域本身毫無關係,那是地圖的邏輯,這個邏輯是否存在取決於策劃是否設計了。

“隸屬於這個區域的怪物”這個索引信息也是非常有用的,因為刷怪的邏輯會非常依賴於這個,常見的用法是:限定這個區域的怪物總數,比如有Boss的時候這個區域最多隻能有10個怪,沒有Boss的時候可以有20個怪,類似這樣的設計並不是不允許的;以及限定這個區域某些怪物的數量,比如這個區域雖然這個時候可能有15個怪物,但是要確保最多隻有6個哥布林,就需要這個數據作為依據了。

“刷怪倒計時”之所以是一個array,是因為會有多個倒計時,每個倒計時結束的時候會刷一次怪,當然這個可以更精細的記錄下掛掉的怪物的信息和刷怪剩餘時間,如果策劃需要的話,但其實這樣做是有點違背這個區域信息的邏輯的。

“刷怪條件(spawnInfo)”即這個區域的刷怪篩選條件,也就是“原本的簡單設計”中的“刷什麼怪”的“複雜版本”,或者更確切的說是“精確版本”。每次刷怪具體刷什麼怪,其實都是走這裡來決定的,當然這裡有一個更簡單的做法,就是拋出腳本函數給策劃:

characterObj SpawnMob(刷怪需要的數據)

把這個刷怪信息拋給策劃,讓策劃返回給我們一個characterObj,其實這是我們真正需要的東西。

  • 刷怪條件SpawnInfo

如果策劃不想用代碼解決問題(或者說代碼用在更深的地方),那麼我們就需要這個SpawnInfo,每一條SpawnInfo代表“這次刷怪的可能性之一”。它主要包含了:

  • 刷怪條件Array<Object>:這個當然是可以組織數據的,但是最好他們都指向一個函數名,而這些函數的返回值是Boolean,這樣就是剛才說的“代碼用在更深的地方”當這些條件全部被滿足的時候,這條刷怪數據才有可能被啟用。
  • 候選怪物信息MobSpawnInfo:這是這條信息裡面可能刷的怪物的規則,依照這條信息,我們可以把一個怪物的填表數據MobModel變成運行時的怪物characterObj。所以除了怪物的模板索引(tag或者id),還有一些怪物的動態數據。這裡要提到的是,如果用tag,那麼就要有一個符合tag的怪物的篩選規則,也許還會引申出其他的數據,具體看需求。

最終這個List<SpawnInfo>也只是為了獲得一個characterObj,即這個怪物,丟到地圖上,以及“隸屬於這個區域的怪物”數組裡。

  • 角色相關數據

如果仔細看這個流程圖,你會發現不管是玩家角色,還是怪物,在這裡都是characterObj,的確沒錯,因為在這個邏輯世界裡活動的,都是角色,至於這個角色受到誰控制,是控制層的問題,與這個數據邏輯沒有任何關係。

雖然characterObj的數據都是一樣的,但是他們的來源未必非得一樣,比如玩家的角色,可能數據來自於數據庫保存的信息;而怪物的數據則是由怪物表(mobModel)的信息,配合一些其他信息而產生的,這些信息的組合,在這個腦圖裡就是mobSpawnInfo。

所以,首先我們分清楚characterObj和mobModel這兩個東西,characterObj是一個runtime的數據,即隨著遊戲變化,這個數據總是在變化的;mobModel是來自數據表的,靜態數據,這些數據無論遊戲怎麼進展都是不會變化的。所以有一些非常動態的數據,他根本就不該屬於怪物表,比如:

  1. 等級:最典型的就是等級,58級的長槍兵和52級的長槍兵,最直接的區別就是等級,以及由等級造成的一些數據不同。
  2. 掉落:在不同的地方的長槍兵,掉落可以是完全不同的,哪怕“不同地方”指的僅僅只是不同的刷怪區域,甚至是同一個區域不同概率的2種長槍兵。
  3. 所屬陣營:這個在腦圖裡面沒有,但是如果一個遊戲夠複雜的話,應該有這個,即刷出來的怪物屬於什麼陣營,陣營不是wow裡面的陣營概念,而更像即時戰略裡面PlayerX的X,不同的X通常都是敵對的,當然如果遊戲要在複雜一些,可以有一個類似星際爭霸的Force的概念,即多個陣營之間有22關係(這個不詳細展開)。
  4. AI腳本段:正如我們之前說的,不同時候刷出來的怪,他們可能在一些AI行為上是不同的,所以他們會被“插入”不同的AI腳本段,至於為什麼用“插入”,怪物的AI應該如何設計?這就是另外一個話題了,相信我,篇幅不會比本篇短,故在此略過。
  5. Buff添加信息:在怪物被刷新後,要給他添加一些Buff,作為他默認的buff,我們需要添加這些buff的信息。這個用途很好理解,比如有些長槍兵刷出來的時候會帶有“狂怒”狀態。但是有另外一些不應該通過這裡去刷,比如刷在下雨的地方,這些怪物就有“不會著火”的特性,就是應該通過下雨的aoe添加了“不會著火”buff(在怪物刷新出來的時候就觸發了aoe.onCharacterEnter,在這裡添加了這個buff,所以不該在刷怪的時候給他添加,當然就算添加了也就那樣了,邏輯設計得好影響不大,寫法和嚴密性問題),當然他們走到乾燥的地方也可以通過aoe把這個buff移除了。
  6. 刷新時間:圖中沒有,而這也並不見得是一個簡單的數字,因為他可能會依賴於其他運行時數據,比如當前的玩家數量等,一樣,我們需要的是返回一個時間(float或Int)用來做倒計時就行了。

這些都是非常典型的,應該屬於運行時的數據,而這些數據的來源應該是根據遊戲實際運行的狀態去根據邏輯進行賦值的,他們絕對不該出現在怪物表(mobModel)裡。

反過來驗算一下

經過上面的思路整理,我們差不多已經可以確定了很多數據的結構,接下來在動手寫文檔、建表、寫代碼之前,我們還需要做一件事情——帶著我們之前的腦洞,回過頭來看看這些功能能否實現,以及一些相關的玩法功能能不能實現,要儘可能的刁難自己,因為越是刁難,越是會出現邊際情況,越是可以催促我們返回去進一步設計。

  • 試試看腦洞滿足了嗎?

現在,我們把那些剛開始想過的腦洞拿出來看看

  1. 不同天氣、不同時間刷不同的怪怎麼做?刷怪條件(spawnInfo)->刷怪條件信息,完全可以滿足我們這個微不足道的要求,有些條目在下雨的時候就不會被啟用,而另一些相似的條目只有在下雨的時候開啟。比如不打傘的步兵只有下雨的時候刷(條件為非下雨天),打傘的步兵只有下雨的時候刷(條件為下雨天)。
  2. 同一個區域刷不同的怪怎麼做?因為刷怪條件是一個列表,所以這根本就不是一個問題了。
  3. 不同區域的怪物要不同表現怎麼做?由於插入AI是刷怪條件(spawnInfo)->候選怪物信息(mobSpawnInfo)下的數據,所以火堆邊的怪物和寨子門口的怪物的“插入AI段”數據不同即可做到。
  4. 根據玩家人數決定刷新數量和時間怎麼做?既然我們可以獲得玩家人數的數據,還可以設置刷怪條件,這就不是問題了,甚至如果玩家數據清晰,還可以根據玩家所選擇的職業比例來刷怪,當30%+玩家是戰士的時候刷弓箭手行不行?當然沒問題,如果策劃認為這個設計合理的話。
  5. 小兵的互動怎麼做?既然一個“地圖區域信息”中有“隸屬於這個區域的怪物”的信息,那做這個本身就不是困難的事情,何況還有萬能的buff機制,在刷怪的時候添加buff……都可以實現這樣的需求。
  6. 天下雨了小兵行為變化了怎麼做?這個問題於刷怪沒有直接關係,但是可以通過AoE和Buff來輕鬆實現。

每一個問題,我們只需要代入性的思考1、2個情景就行了,因為具體的情況太多,如果卡死在驗算的空想階段就太糟糕了,不如等做完了我們實際遇到問題實際解決。

  • 周圍玩法會矛盾嗎?比如我的任務系統?

我們會最先想到的一個問題應該是刷怪和任務的關係了吧?如果刷怪是隨機的,那麼會不會影響任務呢?

回答是當然會。假如你設計的是殺長矛手多少個,那麼刷出長矛手的數量就要通過這些數據控制好,不然玩家就會因為刷怪問題導致不同時候體驗同一個任務的“難度”很不一樣。當然我還是建議一點,也是最常見的遊戲做法,把“殺死長矛手20個”變成“殺死山賊20個”,這樣這個寨子裡刷出來的人如果都帶有山賊的tag,不管是長矛手還是弓箭手或者刀斧手,都算數量,這才是正常的玩家體驗。

同樣的,我們更深入的思考一個問題——這還讓一種任務變成了可能,即“殺傷山賊的士氣”,目標為把山賊的士氣降低到0%,實際的做法是,玩家接受任務的時候獲得一個buff,這個buff到達100層的時候,任務完成,屏幕上顯示的是“山賊士氣<100-buff層數>%。”,當你殺掉一個長矛手的時候疊加這個buff3層,弓箭手2層,刀斧手5層,是不是殺不一樣的怪完成速度就不一樣了?

  • 別再多想了,可以動手試試看了

想到這裡就別多想了,該動手開始實現了,不過不論你從設計到實現的時候,一定不要忘記一個要點——猴叔的機制最大的不同是——設計這些做法為的是讓人更容易發揮,所以從一開始就應該考慮的是如何更容易維護的開放式思維,而不是開始就想好有哪些約束,讓別人只能在約束下設計,這樣是有違設計精神的

專欄地址:https://zhuanlan.zhihu.com/p/53806267

相關推薦

推薦中...