【源碼看饑荒】第一期,世界的構(gòu)成
萬物起源
當(dāng)你開了一個(gè)新檔,第一次進(jìn)入游戲的時(shí)候,會有一個(gè)生成世界的步驟。這個(gè)過程可以稱之為世界的初始化。系統(tǒng)首先繪制世界地圖,將各種地皮串聯(lián)起來。然后在每一個(gè)地皮上放置一些資源和奇遇。世界地圖是隨機(jī)生成的,但也是有一定的規(guī)律,可以進(jìn)行設(shè)置的。掌握了編程的方法后,你就可以繪制出自己想要的世界地圖了。不過本期的重點(diǎn)并不在于此,后續(xù)會有一期專門講解地圖。這里想說的是各種資源,比如花、草、蜜蜂、蝴蝶、豬人等等。他們都有一個(gè)統(tǒng)一的稱謂:Prefab,翻譯成中文叫作預(yù)制物。除了各種資源之外,各種特效、建筑物,那些可以與玩家發(fā)生互動的或者不能互動的,看得見的或者看不見的東西,都可以稱為Prefab。另外,我們還可以把Prefab理解為生物學(xué)里的染色體,它的基因記錄了一類物體的初始化方法(應(yīng)該如何生成,包含外觀、各項(xiàng)屬性以及行為方式等等)。系統(tǒng)會根據(jù)染色體上的基因來將Prefab生成實(shí)體,投放到游戲系統(tǒng)里。每種東西都有自己的Prefab,有著唯一的名字,有一條控制臺命令用于生成Prefab:c_spawn("{prefab}", amount),這里面的{prefab}就是就是指Prefab的名字,比如spider,pigman等等。有一些物種之間很相近,比如各類蜘蛛,他們會共用一部分初始化的內(nèi)容,但也會有各自的差異。除了使用游戲提供的Prefab,我們也可以根據(jù)需要,自己定義新的Prefab。這需要給Prefab起一個(gè)名字,提供一個(gè)初始化方法,最后再調(diào)用系統(tǒng)的函數(shù)進(jìn)行注冊,讓系統(tǒng)知道有了一個(gè)新的Prefab。
游戲世界里豐富多彩的各種東西,本質(zhì)上都是由各種Prefab生成的實(shí)體,它們之間的核心區(qū)別,就在于染色體上的基因,也就是初始化方法。Prefab是多種多樣的,它們的初始化方法也各不相同,但也有一些共性。接下來就講講,Prefab的初始化都有哪些內(nèi)容。如果有興趣的同學(xué),也可以直接閱讀源碼文件夾下的Prefabs文件夾。
靈魂
首先第一步,添加靈魂,也就是創(chuàng)建一個(gè)實(shí)體,這是實(shí)現(xiàn)與游戲世界交互的第一步。在這一步里面,系統(tǒng)會賦予實(shí)體一個(gè)唯一的ID,這是與其他物體區(qū)別的標(biāo)識,即使屬于同一個(gè)Prefab,也擁有各自不同的實(shí)體。除了賦予實(shí)體ID之外,系統(tǒng)還會分配相應(yīng)的內(nèi)存,記錄這個(gè)實(shí)體的各種信息,比如創(chuàng)建時(shí)間,存檔信息等等。在大多數(shù)Prefab的初始化方法中,第一行代碼都是local inst = CreateEntity(),這里的CreateEntity就是系統(tǒng)提供的一個(gè)創(chuàng)建實(shí)體的方法,調(diào)用它會獲得一個(gè)由游戲引擎創(chuàng)建的實(shí)體,同時(shí)還會記錄實(shí)體id,創(chuàng)建時(shí)間等基本信息。這個(gè)實(shí)體,我們用inst來指代。注意,這個(gè)inst實(shí)際上也是個(gè)容器,用于存儲各種數(shù)據(jù),在CreateEntity方法中,真正返回的實(shí)體是被存在inst.entity下。在這一步創(chuàng)建的實(shí)體,只有一個(gè)靈魂和一些創(chuàng)建時(shí)記錄的信息,既看不到,也做不了任何事情。
血肉
第二步,添加骨骼和血肉。實(shí)際上也就是為實(shí)體添加各種組件,并設(shè)置每個(gè)組件的初始化參數(shù)。組件可以分為兩類,一類是底層游戲引擎提供的組件,比如位置組件Transform,動畫組件AnimState等等,在聯(lián)機(jī)版中為了實(shí)現(xiàn)機(jī)器之間的數(shù)據(jù)交換,還有網(wǎng)絡(luò)組件Network,這些組件由游戲引擎提供,通過調(diào)用inst.entity.AddXxx來獲得,數(shù)量不多,但都是與游戲引擎強(qiáng)相關(guān)的核心組件(比如位置、動畫、光照、網(wǎng)絡(luò))。它們沒有l(wèi)ua語言定義的源碼,無法看到具體的功能,只能通過名字和參數(shù)去理解,然后模仿游戲里已有的lua代碼的用法來使用。另一類是通過lua語言定義的組件,這些組件首先需要被聲明為Component類,并給出一個(gè)唯一的名字,然后在Prefab的初始化方法中執(zhí)行inst:AddComponent("{component}"),這里的{component}就是組件的名字。
添加組件,是Prefab初始化方法中代碼占篇幅最大的內(nèi)容,常??梢哉嫉?0%以上。可以說,組件是染色體上的主要基因。組件種類以及初始化參數(shù)的不同,造就了這個(gè)世界豐富多彩的種群。比如人物都會有飽食和精神組件,但怪物沒有。動物都有血量的組件,植物卻沒有。即使有著同樣的組件,不同的Prefab也有不同的初始化參數(shù)。比如人物都可以吃食物,但機(jī)器人WX78還可以吃齒輪。游戲Mod中數(shù)量最多的當(dāng)屬人物Mod,每個(gè)人物都有自己獨(dú)特的形象,這是因?yàn)樗麄兺ㄟ^inst.AnimState.SetBuild('{build_name}')讀取了相應(yīng)的設(shè)置。而一些比較有特色的人物,作者往往自己編寫一個(gè)專屬組件。比如我就為Samansha編寫了組件Photosynthesis,用來實(shí)現(xiàn)Samansha的光合作用。組件是游戲機(jī)制的核心。各種功能不同的組件,為我們帶來豐富多彩的游戲體驗(yàn)。比如Transform與位置相關(guān),像是蟲洞瞬移之類的動作,都需要借助Transform;AnimState與外形和動畫相關(guān),特定條件下改變?nèi)宋锿庑?,不同地動作下播放不同的動畫,都需要借助AnimState;而游戲最基本的三圍——飽食度、精神值、血量分別對應(yīng)著hunger, sanity, health三個(gè)組件。只要能了解這些清楚各種組件的作用,就能極大地提升對游戲核心機(jī)制的理解。
動起來
第三步,添加狀態(tài)圖,也就是SG,全稱是StateGraph,在業(yè)界更常用的稱呼是有限狀態(tài)機(jī)(finite-state machine,F(xiàn)SM)。這一步是可選的,對于一些比較簡單的Prafab,比如特效,在生成之后,只播放了一遍動畫之后就自動被移除了,很容易描述,就不需要設(shè)置SG。但對于玩家這樣,或者一些有復(fù)雜行為的BOSS,能做出的行動非常多,每個(gè)行動下都會有不一樣的表述。比如玩家,靜止不動時(shí),偶爾會打個(gè)哈欠;在地圖上走動時(shí),會動起手腳,踩到某些地板還會發(fā)出響聲,自己的位置也會移動;戰(zhàn)斗時(shí),會揮舞武器進(jìn)行攻擊。這里的狀態(tài)-State,指的是像「靜止」、「走動」、「攻擊」這樣一個(gè)整體的概括。前面說到,游戲的核心處理邏輯是在組件里寫的,比如攻擊怪物會扣多少血,一秒內(nèi)可以移動多遠(yuǎn),都是通過對應(yīng)組件來完成的。這里的State承擔(dān)的主要功能是,寫清楚這個(gè)狀態(tài)下應(yīng)該播放什么動畫和音效,觸發(fā)相應(yīng)組件的計(jì)算邏輯,在這個(gè)狀態(tài)下的逐幀變化,以及如何轉(zhuǎn)化為其他State等等。通常來說,有多套動畫的生物,都會有自己的SG,并且哪怕是同一個(gè)State,不同的生物也有不同的表現(xiàn),比如各種怪物都會有基礎(chǔ)的「攻擊」State,但每種怪物的攻擊動畫都是不一樣的。攻擊的「前搖」、「后搖」在不同怪物上的差異,主要就是通過SG來實(shí)現(xiàn)的??梢哉f,SG決定了如何、何時(shí)觸發(fā)傷害計(jì)算邏輯,而組件則決定了具體傷害的數(shù)值以及造成傷害后的效果。細(xì)心的玩家肯定會發(fā)現(xiàn),人物拿不同的武器進(jìn)行攻擊與空手攻擊之間的攻擊頻率是有所差別的,這是因?yàn)椤腹簟惯@個(gè)狀態(tài)是有一個(gè)冷卻時(shí)間的,在冷卻時(shí)間內(nèi)無法再次攻擊。每一幀的時(shí)間為1/30秒,各類武器的冷卻時(shí)間如下:鞭子17幀、書19幀、露西斧11幀、一般武器、提燈、海貍撕咬為13幀、空手為25幀??梢悦黠@看出,空手的冷卻時(shí)間很長,幾乎是兩倍于一般武器,所以能很明顯地感受到攻擊頻率的不同。大多數(shù)人物都是用同一套SG,如果想要改變?nèi)宋锏墓纛l率的話,就需要修改SG中相應(yīng)的「攻擊」?fàn)顟B(tài)的定義。
對State的定義,可以決定在一個(gè)State里動畫、音效、組件觸發(fā)的各種情況,乃至于在一個(gè)State結(jié)束后應(yīng)該轉(zhuǎn)向哪一個(gè)State,但不能完全覆蓋State之間的轉(zhuǎn)換。比如玩家進(jìn)入到了「攻擊」的State,剛剛抬手就被怪物的攻擊打斷,則會進(jìn)入「硬直」的State,還有玩家在抬手攻擊時(shí),發(fā)現(xiàn)自己血量很低,馬上吃了補(bǔ)血的食物,從而進(jìn)入「進(jìn)食」的State。這些類型的State轉(zhuǎn)換都是通過觸發(fā)某種事件(比如被攻擊)或者動作(比如進(jìn)食)來實(shí)現(xiàn)的。在代碼的層面上,通過事件觸發(fā)State轉(zhuǎn)換,稱之為EventHandler,通過動作觸發(fā)State轉(zhuǎn)換,稱之為ActionHandler。事件觸發(fā)State的機(jī)制,主要是為了方便各種組件進(jìn)行State管理,比如戰(zhàn)斗組件,只要在受到攻擊傷害時(shí),發(fā)送一個(gè)「被攻擊受傷」事件,就能自動觸發(fā)自身進(jìn)入「硬直狀態(tài)」。而動作觸發(fā)State的機(jī)制,主要是為了玩家操作考慮。玩家與物品發(fā)生交互的過程中必然會產(chǎn)生動作,這個(gè)動作直接推動SG進(jìn)入相應(yīng)的State,播放對應(yīng)的動畫和音效。
研究SG,對于Mod制作者的意義在于,能夠很方便地管理一些復(fù)雜生物的行為。將一個(gè)行為的各項(xiàng)代碼組合到一個(gè)State里,在程序設(shè)計(jì)上會顯得更為清晰,也容易維護(hù),還可以實(shí)現(xiàn)逐幀操作,決定何時(shí)觸發(fā)動畫、音效的播放和組件邏輯的計(jì)算。而對于一般的玩家來說,則可以更深入地了解生物的行為細(xì)節(jié),比如怪物的攻擊頻率和前搖后搖,硬直時(shí)間等等,從而更好地進(jìn)行戰(zhàn)斗。
下面的圖是一個(gè)典型的狀態(tài)圖,簡單直觀容易理解

AI決策
第四步,添加AI。在玩家操作的角色之外,所有的生物都有自己的一套行為邏輯,也就是我們常說的AI。實(shí)現(xiàn)AI的方式有很多,如果判定標(biāo)準(zhǔn)和行為都比較簡單,用上面說到的SG來實(shí)現(xiàn)就可以。但如果行為比較復(fù)雜,就需要其他的方式來實(shí)現(xiàn)了。在這個(gè)游戲中,復(fù)雜的AI是用「行為樹」實(shí)現(xiàn)的,實(shí)質(zhì)上就是一個(gè)樹形結(jié)構(gòu)圖,每個(gè)節(jié)點(diǎn)代表一個(gè)行為或者條件判斷邏輯,分支代表著不同條件對應(yīng)的下一步行動。在游戲的源碼中,與AI相關(guān)的代碼有兩部分,存儲在兩個(gè)文件夾下,分別是brains和behavious,前者代表著一個(gè)生物完整的行為樹定義,后者則是具體到某個(gè)行為節(jié)點(diǎn)的形式邏輯。比如brain會決定兔人發(fā)現(xiàn)玩家持有肉類時(shí),發(fā)起「攻擊」行為。而behavious則會詳細(xì)描述如何發(fā)起攻擊,比如說要不要追擊玩家,追多遠(yuǎn)會返回等等。
AI的理解和編寫,還是相對復(fù)雜的,后續(xù)會專門寫一篇來解讀。研究清楚AI,對于Mod制作者來說,可以實(shí)現(xiàn)一些很智能的操作,比如讓寵物自己干活。對普通玩家來說,則是能清晰地了解生物的行動模式,更好地去與各種生物進(jìn)行互動。與SG的區(qū)別在于,SG主要強(qiáng)調(diào)狀態(tài)的變化細(xì)節(jié)(比如在哪一幀觸發(fā)傷害),而AI強(qiáng)調(diào)的是行為的決策(比如什么條件下會發(fā)起攻擊)。
下面的圖是一個(gè)簡單的行為樹,從根節(jié)點(diǎn)起,從左到右依次遍歷子節(jié)點(diǎn),直到執(zhí)行成功(不滿足條件的節(jié)點(diǎn)就是執(zhí)行不成功,比如周圍沒有食物,則這個(gè)節(jié)點(diǎn)執(zhí)行不成功,不會往下,而是接著訪問下一個(gè)兄弟節(jié)點(diǎn)-是否有主人)

世界地圖
除此之外,Prefab的初始化內(nèi)容還包括添加TagAddTag和注冊事件監(jiān)聽回調(diào)ListenForEvent,但這些內(nèi)容不是必須的,通常是為了特定目的而設(shè)置,沒有太多共性,在下一期專門介紹Prefab的內(nèi)容中才會詳細(xì)解釋。
綜上總結(jié),Prefab 可以說是世界構(gòu)成的基本元素,可以理解為成類似染色體的模板,讓系統(tǒng)能快速生成一批同種類的東西,比如一群蜘蛛或者豬人或者一片森林。Prefab 以組件實(shí)現(xiàn)各種游戲邏輯,通過SG來控制自身動畫、音效的播放、通過行為樹來實(shí)現(xiàn)AI。
了解了 Prefab 之后,我們就可以來談?wù)劯蟮膬?nèi)容了,那就是世界地圖的生成方式。
在世界地圖的生成過程中,Prefab是最小的單位,比如樹木、干草、灌木、怪物、動物、石頭等等,系統(tǒng)以某種方式將這些Prefab分散到地圖上。在 Prefab 之上,是 Static Layout,中文為靜態(tài)布局,可以理解為幾個(gè)Prefab以固定的靜態(tài)布局展現(xiàn)出來的東西。比如豬王就是一個(gè)典型的 Static Layout,在豬王周圍總是有方尖碑,而且位置方向都是固定的。如果靜態(tài)布局再加上能以某種方式觸發(fā)的事件,就成了Set Piece,也就是我們常常說到的彩蛋。再往上一層,被稱為Room。Room是最小的隨機(jī)生成單位。一個(gè)Room,可以理解為一塊小區(qū)域,它會有統(tǒng)一的地皮,還可以設(shè)置隨機(jī)或固定數(shù)量的Prefab、Static Layout。同樣是森林地皮,有的區(qū)域樹很多,有的卻很少,某些還會有海象巢穴,這是因?yàn)樗鼈儗儆诓煌腞oom。在Room之上,是Task,一個(gè)Task內(nèi)可以包含若干個(gè)Room,也可以理解為是一個(gè)大區(qū)塊。比如一大片草原,就是一個(gè)草原的Task,會有很多個(gè)不同的Room,某些Room有很多牛,而另一些則有很多兔子。再往上,是Task Set,實(shí)質(zhì)上就是一整個(gè)世界,會囊括很多個(gè)Task。不同的Task Set有不同的地圖設(shè)計(jì)和生成機(jī)制,比如一般游戲模式下進(jìn)入游戲的世界被稱為forest,而敲碎地洞進(jìn)去的世界,被稱為cave,它們各自代表著一個(gè)Task Set,很明顯兩者的地圖是大不相同的。
那么世界是如何生成的呢?首先,根據(jù)玩家的設(shè)置,獲取對應(yīng)的Task Set,如果沒有進(jìn)行修改,默認(rèn)會取用名為forest的Task Set。然后根據(jù)Task Set的設(shè)置,生成若干個(gè)Task,并以某種機(jī)制把這些Task串聯(lián)起來。然后在每個(gè)Task上生成對應(yīng)的Room,最后在每個(gè)Room上放置適當(dāng)數(shù)量的Prefab和Static Layout,世界的生成就完成了。
對于Mod制作者來說,如果你希望設(shè)計(jì)獨(dú)特的地圖,或者添加某些特定的區(qū)域,就需要研究世界地圖是怎樣生成的,比如我的Samansha人物Mod,就添加一個(gè)區(qū)域來生成特有的鹿。對一般玩家來說,了解世界的生成機(jī)制,就有利于快速找到一些有用的資源,比如海象屋,沼澤地蘆葦叢等等。
交互界面
到這里,世界的構(gòu)成就已經(jīng)講完了,但我還想要再補(bǔ)充游戲的交互界面的知識。進(jìn)入游戲后的各種按鈕,狀態(tài)指示器等等,都被稱為Widget,中文叫小部件。它就是很小的一個(gè)元素,為了特定的需求服務(wù),比如顯示飽食度,或者瀏覽一個(gè)箱子的內(nèi)部儲存等等。而一些要占據(jù)整個(gè)屏幕的東西,比如選人畫面、小地圖畫面等,就被稱為Screen。
對于Mod制作者來說,想要實(shí)現(xiàn)一些特殊的UI操作,就必須了解Widget和Screen。而對于一般玩家來說,了解這些,主要是弄清楚各種按鈕的交互作用。
好了,本期的介紹就到這里,下一期會詳細(xì)講解Prefab,除了更細(xì)致地講解Prefab的生成之外,還會介紹各種典型的Prefab,歡迎關(guān)注。