最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

第二期 entity與prefab

2021-09-16 19:50 作者:LongFei_gamer  | 我要投稿

引言


entity,中文一般稱為實體,指的是在游戲中出現(xiàn)的一切看得見和看不見的物體。比如控制世界變化的TheWorld,就是一個看不見的實體,隨處可見的花草,也是實體。每個實體經(jīng)由系統(tǒng)生成,會分配一個游戲全局唯一的ID,稱為GUID(GLOBAL Unique ID)。 創(chuàng)建實體沒有任何門檻,在創(chuàng)建之后你也可以隨意改造。同樣的,也可以改造一個已有的實體,你甚至可以把一個豬人變成蜘蛛。如果我希望生成一只蜘蛛,首先是創(chuàng)建一個實體,然后給它添加蜘蛛的動畫,再加上蜘蛛具備的一些能力,添加SG和brain。但是,如果每次在需要生成某一類entity(比如蜘蛛)的時候,都要寫上大段的代碼(下文簡稱為初始化代碼),就很不方便。而且也不利于后續(xù)維護修改,比如要調(diào)整蜘蛛的屬性,就需要在每一個生成代碼里都進行修改。 為了解決這個問題,最直接的想法是,把初始化代碼寫成一個初始化函數(shù),每次要生成蜘蛛的時候,就調(diào)用這個函數(shù)。這樣一來,無論是使用還是維護,都方便了許多。但仍然存在問題:如果要生成的entity類別很多,有幾百個,那就得寫幾百個函數(shù),這些函數(shù)名要維護起來也會有點麻煩。另外,entity還需要關(guān)聯(lián)動畫、音效等資源,如果這部分代碼寫在創(chuàng)建函數(shù)里,也不太合適。 更進一步的方案是,制作一個模板,存放所有能體現(xiàn)這個entity獨特性的東西,包括各種資源和初始化代碼,并且為這個模板取一個唯一的名字,然后把這個名字注冊到系統(tǒng)中。然后構(gòu)建一個生成函數(shù),可以根據(jù)模板信息自動生成相應的實體。當系統(tǒng)需要生成這一類實體的時候,直接根據(jù)名字找到模板來生成就可以了,這就是prefab的由來。這是個經(jīng)典的游戲編程概念,中文名一般稱為預制物或預制體。

prefab與entity的區(qū)別在于,entity是實實在在會占用大量系統(tǒng)資源,每一個entity都是獨立存儲計算,有自己的數(shù)據(jù),數(shù)量越多,消耗的資源越多。而prefab只是個模板,僅僅占用很小的固定資源,在游戲過程中不會發(fā)生變化。 對編程有基本了解的同學,都知道class這個經(jīng)典的概念。class與prefab之間也有區(qū)別。prefab一經(jīng)定義,就無法在游戲運行中再做修改。而在許多解釋型語言中,都提供了在運行中動態(tài)修改class的能力。

簡而言之,各種各樣不同的Prefab作為模板,根據(jù)需要生成了各種entity,從而構(gòu)成了豐富多彩的游戲世界。

本篇會從源碼的角度,剖析prefab是如何生成的。

注:下文涉及代碼的地方都會給出文件的相對路徑,默認根目錄是游戲的代碼文件夾,也就是scripts

prefab的定義

所有的prefab都通過Prefab類來構(gòu)建,實質(zhì)上是Prefab類的一個實例。 Prefab類的定義代碼寫在prefabs.lua文件里,完整的構(gòu)建函數(shù)形如Prefab(name, fn, assets, deps, force_path_search),這幾個參數(shù)的含義如下

  • name: prefab的名稱,必須是唯一的,不能與其它prefab的名稱重復。這個名稱會注冊到系統(tǒng)里,可以在游戲控制臺中通過c_spawn("{name}")來生成對應的prefab

  • fn: prefab的初始化函數(shù),當系統(tǒng)生成相應prefab的實體時,會執(zhí)行這個初始化函數(shù),生成對應的prefab。不同prefab的區(qū)別就在于這個初始化函數(shù)的不同

  • assets: 相關(guān)的動畫、音效等靜態(tài)資源文件的路徑

  • deps: 相關(guān)的Prefab依賴

  • force_path_search: 強制搜索路徑

大多數(shù)情況下,都只會用到name, fn, assets這三個參數(shù),deps和force_path_search一般是可以忽略的。

而在這其中,最重要的就是初始化函數(shù)fn,它描述了一個Prefab在生成時應該如何設(shè)置,比如使用何種動畫骨架,最開始的外形如何,是否能發(fā)起攻擊等等,如果有HP的話,初始化的血量是多少等等。如果你想了解一個物體的詳細參數(shù),最直接有效的方法就是找到這個物體的Prefab的初始化函數(shù),仔細閱讀。

初始化函數(shù)fn

初始化函數(shù)的代碼,在復雜的Prefab中通常比較龐雜,像生成人物的代碼就有足足1000多行,如果只是單純地逐行閱讀,是比較吃力的。事實上,代碼的編寫是有一定的思路的,總的來說,可以把其中的內(nèi)容分類為以下幾塊。

  • 創(chuàng)建實體:提供一個實體,后續(xù)的一切操作都圍繞著這個實體來進行

  • 系統(tǒng)底層組件:添加基本功能組件,這些組件的源碼無法看到,僅暴露可調(diào)用的函數(shù)

  • tag:打上標簽,主要用于對Prefab進行分類,方便系統(tǒng)對特定類的Prefab進行操作

  • StateGraph和Brain:在Prefab的初始化中一般分別只有一句,用于設(shè)置對應的StateGraph和Brain

  • Component:添加組件,與系統(tǒng)底層組件的區(qū)別在于可以看到源碼。

  • 監(jiān)聽:設(shè)置對事件或者世界狀態(tài)(比如是否月圓之夜,白天還是晚上)的監(jiān)控,當觸發(fā)特定事件或者進入某個狀態(tài)后,執(zhí)行相應的操作

  • 其它設(shè)置:其它個性化的配置

除了必須以「創(chuàng)建實體」作為第一句代碼外,其它的部分一般來說并沒有嚴格的順序限制。要加什么,完全取決于你希望這個Prefab長什么樣,能做什么,在這一點上是非常自由的。在聯(lián)機版中,客機的數(shù)據(jù)需要從主機同步,許多場景的數(shù)據(jù),比如人物的血量、飽食度等,都是來自Component的。因此,在客機中,Component是不需要添加的,即使強行添加,也不會被使用。類似的還有StateGraph和Brain。因此,在聯(lián)機版的Prefab定義中,通常會有以下這段代碼

這段代碼的意思是,檢查當前系統(tǒng)環(huán)境是否是主機。如果不是主機,就到此為止,不再執(zhí)行后面的代碼。在這段代碼之前的部分,是主客機通用的,通常只包含創(chuàng)建實體,添加系統(tǒng)底層組件,添加tag。

雖然代碼的編寫順序沒有嚴格的限制,但官方的代碼多數(shù)是按照上面的順序來編寫的,其中添加StateGraph和Brain的代碼通常只有一句,通常寫在component的部分之前。有一個例外是locomotor,根據(jù)官方代碼給出的注釋,這個component需要在添加StateGraph之前就添加。

聯(lián)機版的典型代碼的示例如下

下面就分塊來講解這些代碼,這里選用的示例Prefab是rabbit,代碼在prefabs/rabbit.lua。它不像豬人、蜘蛛那樣有多種形態(tài)寫在同一個Prefab文件里,整體代碼結(jié)構(gòu)比較清晰,以上講到的部分都有涉及,代碼量適中(整個腳本共300~400行),是很好的學習參考對象。

創(chuàng)建實體

實體創(chuàng)建只有一行代碼:local inst = CreateEntity(),由此得到一個inst變量,它代表著Prefab初始化的實體,后續(xù)的所有操作,無論是添加組件還是tag,都需要圍繞這個實體來進行。

在這個部分,順帶講講CreateEntity的具體執(zhí)行內(nèi)容和inst對應的類

CreateEntity是定義在lua腳本代碼里的,可以在mainfunctions.lua下面找到,定義如下

EntityScript這個類的定義,則可以在entityscript.lua里找到,上面初始化構(gòu)建EntityScript的實例時傳入的ent,可以通過inst.entity調(diào)用

根據(jù)上面代碼可以看到,我們得到的inst實際上是用lua腳本定義出來的EntityScript類的一個實例,對真正實體的一個封裝。真正的實體是通過系統(tǒng)底層的函數(shù)TheSim:CreateEntity來創(chuàng)建的。當我們要使用系統(tǒng)底層的組件時,必須通過這個真正的實體去調(diào)用。因此,在添加底層組件的時候,使用的是這樣的的格式:inst.entity:AddXXX(),這里的inst.entity就是真正的實體。

為什么會做這樣的設(shè)計呢?這是因為系統(tǒng)底層返回的entity所擁有的函數(shù)是比較有限的,要添加或者修改都比較麻煩。在進行entity層面上的修改時,會很不方便。比如添加Component這個工作:Component是直接用lua定義的,自然地,要添加Component也需要通過lua來完成。這中間會涉及到一些重復的,相對有點復雜度的處理,會希望抽象提取成一個函數(shù),也就是AddComponent。希望能通過inst直接調(diào)用這個函數(shù),而系統(tǒng)底層返回的entity又不方便直接修改。因此,使用EntityScript來做一層封裝,在EntityScript這個類中定義AddComponent,從而實現(xiàn)inst:AddComponent('xxx')這樣簡潔的調(diào)用。

系統(tǒng)底層組件

系統(tǒng)底層組件,指的是那些和Component有類似用法,但定義代碼被封裝編譯在游戲引擎內(nèi)部,我們無法看到具體源碼的部分。相比于數(shù)量接近400個的Component,在官方代碼中出現(xiàn)的系統(tǒng)底層組件較少,只有近30個,其中被大量使用的也不到10個。使用底層組件而不是Component的原因可能有很多,每一個決策都有其依據(jù)。比較常見的理由是,組件需要非常頻繁調(diào)用底層系統(tǒng)的其它資源,比如控制動畫、聲音的播放,或者控制entity的位置變化等,這樣的情況下,封裝在底層系統(tǒng),可以有效地提升游戲性能。

在這個部分,代碼可以再細分成兩部分:添加組件和設(shè)置初始化參數(shù)。

具體代碼示例如下

添加組件的代碼比較簡單,每個組件單獨添加,格式統(tǒng)一為inst.entity:Add{組件名}()

然后是設(shè)置初始化參數(shù),格式統(tǒng)一為inst.{組件名}:調(diào)用函數(shù)(參數(shù)列表...)。比如inst.DynamicShadow:SetSize(1, .75),就是調(diào)用了在設(shè)置「動態(tài)影子」組件DynamicShadow,使用SetSize函數(shù)來設(shè)置影子的大小。

并不是所有的組件都需要做初始化。比如Network組件,是負責主客機通信的,只有在主客機之間發(fā)生數(shù)據(jù)交流時才被使用,不需要設(shè)置初始化參數(shù)。

MakeCharacterPhysics(inst, 1, 0.5)這一句代碼,實際上是一個封裝好的Physics組件初始化函數(shù),代碼可以在standardcomponents.lua下找到。這個函數(shù)的含義是為entity添加一個Physics組件,并設(shè)置一系列的初始化參數(shù),構(gòu)成一個生物的物理效果。

問題在于,看不到源碼的情況下,怎么確定都有哪些系統(tǒng)組件可用,每個組件都有哪些函數(shù)呢?如果完全沒有其它內(nèi)部資料,唯一的手段就是從官方源碼來學習。通過對官方源碼的全方位分析,可以發(fā)現(xiàn)共使用了29個不同的底層組件, 其中調(diào)用添加的次數(shù)在20以上的常用組件如下,了解這些組件如何使用就可以了。

  • Transform: 控制空間位置變換

  • AnimState: 控制動畫播放

  • Physics: 物理引擎,控制碰撞體形狀和大小,以及運動狀態(tài)等

  • Network: 控制網(wǎng)絡(luò)數(shù)據(jù)傳輸

  • SoundEmitter: 控制聲音播放

  • MiniMapEntity: 小地圖圖標

  • DynamicShadow: 控制影子

  • Light: 控制光照

  • LightWatcher: 監(jiān)控光照強度

其中物理引擎的部分與其它組建不同,具有標準化的特點:同一類物體都用同一套初始化參數(shù),比如MakeCharacterPhysics不僅僅用在rabbit上,也用在玩家角色、豬人和魚人上。因此,一般都會使用官方自己寫好的標準組件函數(shù)來完成初始化,而不是一條條地調(diào)用Physics組件函數(shù)。

系統(tǒng)底層組件,以及下面會談到的Component,是Prefab里最主要的組成部分,它們決定了一個Prefab有什么功能,每個功能具體如何與系統(tǒng)進行交互。要詳細列出來每個組件的用法,篇幅會比較巨大,因此這部分內(nèi)容會單獨寫一篇文章來進行介紹。

Tag

tag,其實就是一個標記,用于區(qū)分一類Prefab,讓系統(tǒng)能夠針對性地對這一類Prefab作出反應。比如在人物的初始化函數(shù)里,都會添加「player」tag,這樣在進行一些判定的時候,就可以通過檢測這個標記,來有效地篩選出屬于玩家的實體。舉例來說,玩家死后變成幽靈,丟失「player」,換成了「playerghost」,與世界的互動方式也會變成「作祟」。這就是通過檢測tag來區(qū)分給出不同的動作。類似的還有蜘蛛不會主動攻擊韋伯,是因為蜘蛛不會對有「spider」tag的生物發(fā)起攻擊。

在做Prefab初始化的時候,主要是根據(jù)需要添加相應的tag,可以通過inst:AddTag('xxx')來添加。

rabbit的tag相關(guān)代碼如下

一般來說,如果自己做Mod,有定位類似官方生物的,比如我的《Samansha》人物Mod里的鹿,定位和羚羊相似,則應該添加相同的tag。官方給出的許多tag,都能從名字上直觀地看出來是有何含義,會用在什么場景,盡量保持Tag一致,有利于在游戲中取得一致的系統(tǒng)反饋。

到tag為止,往下的代碼都只在主機中執(zhí)行。

StateGraph和Brain

關(guān)于SG和Brain,在第一篇中已有簡略介紹,本篇重點也不在于此,因此不做過多的重復。 在Prefab的初始化中,與SG和Brain相關(guān)的部分就是為Prefab設(shè)置SG和Brain,代碼非常簡單,如下幾句

一般SG和Brain視情況添加。

  • SG:如果所構(gòu)建的Prefab有較復雜的動畫轉(zhuǎn)換,或者就是和某個官方生物使用同一套動畫骨架,則需要設(shè)置SG。

  • Brain:如果希望這個Prefab有一定復雜度的自主智能,能自動判斷什么情況下做什么,則應該添加Brain。

Component

和系統(tǒng)底層組件類似,區(qū)別在于定義的代碼都是可以直接在components文件夾下的同名lua文件看到。Component相比于底層組件,數(shù)量多得多,有將近400個,為Prefab提供了豐富多樣的功能。

添加Component的代碼形如inst:AddComponent("{component_name}"),調(diào)用相應的組件函數(shù)則是形如inst.components.{component_name}:{fn_name}(args)

比如讓rabbit可以吃東西,就寫了以下代碼

這兩句代碼的意思是,添加eater這個Component,讓rabbit具備吃食物的能力。然后通過SetDiet函數(shù)來設(shè)置rabbit能吃哪些種類的食物。

Component這部分的代碼,參考官方的寫法,每添加一個Component就接著寫相應的初始化,這一點與系統(tǒng)組件先添加全部再初始化不同。這是因為Component的組件數(shù)量很多,而且更改也相對頻繁,將添加的代碼與初始化代碼放在一起,更方便管理。

在rabbit中,總共添加了如下幾個Component

  • locomotor:提供移動能力

  • eater: 提供吃東西的能力

  • inventoryitem: 可以裝進玩家的物品欄

  • sanityaura: 可以影響周圍生物的sanity

  • cookable: 可以用于烹飪

  • knownlocations: 能夠記住位置,這里主要是用于能夠找到自己家的洞穴

  • health: 擁有血量

  • lootdropper: 會掉落物品

  • combat: 可以戰(zhàn)斗

  • inspectable: 可以被檢查

  • sleeper: 可以睡覺

  • tradable: 可以用于交易

  • burnable: 會著火

  • freezable: 會結(jié)冰

  • hauntable: 可以被作祟

  • perishable: 會腐壞

因為內(nèi)容過多,不會講解每個component的細節(jié),主要談談以下幾個函數(shù),他們都是來自standardcomponents.lua,可以通過這一個函數(shù)來完成相應目的的配置。推薦在遇到類似的需求時,使用官方提供的這些函數(shù),可以避免因版本變動引起的bug。

  • MakeSmallBurnableCharacter: 能著火,適用于小型生物,類似的還有MakeMediumBurnableCharacter,MakeLargeBurnableCharacter,分別是和中型和大型生物,下面的Freezable也類似

  • MakeTinyFreezableCharacter: 能結(jié)冰

  • MakeHauntablePanic: 能被作祟,被作祟后會逃開

  • MakeFeedableSmallLivestock: 能被喂食,并且長期不喂食會死掉

監(jiān)聽

所謂監(jiān)聽,就是當滿足特定條件的時候,執(zhí)行某些動作。比如rabbit到了冬天會換成白色的冬兔外貌,被攻擊了之后會迅速逃跑到最近的洞穴里,這些變化都是通過監(jiān)聽做到的。

監(jiān)聽的實現(xiàn)方式多種多樣,通常來說,只需要一個合適的觸發(fā)器(Trigger),以及設(shè)定一個在滿足條件觸發(fā)時要執(zhí)行的回調(diào)函數(shù)(Callback Function)即可。

在Prefab的初始化定有中,常用的觸發(fā)器有兩個:

  • 對世界狀態(tài)的監(jiān)聽: WatchWorldState,當世界進入某一個特定的狀態(tài)時,執(zhí)行回調(diào)函數(shù)

  • 對自身事件的監(jiān)聽: ListenForEvent,當自己觸發(fā)了某個事件時,執(zhí)行回調(diào)函數(shù)

在rabbit中的實際應用如下

對世界狀態(tài)的監(jiān)聽

WatchWorldState的代碼鏈路比較深,可以這樣找到:初始化函數(shù)->OnInit->OnWake

這段代碼的含義是,監(jiān)聽世界的狀態(tài)是否是冬天。如果是冬天,會切換成冬兔形態(tài),如果是其它季節(jié),會切換成普通兔子的形態(tài)。

對自身事件的監(jiān)聽

通過inst:ListenForEvent來設(shè)定事件觸發(fā)器,第一個參數(shù)是觸發(fā)的事件,第二個參數(shù)是回調(diào)函數(shù)。 這串代碼的含義是當rabbit自身被攻擊的時候,會觸發(fā)一個attacked事件,然后執(zhí)行這個回調(diào)函數(shù),rabbit就會往自己家的洞穴跑。如果距離自己家太遠,找不到回家的路的話,就不會再跑了。

其它設(shè)置

其它所有不歸屬于上面幾個模塊的都歸屬于此,這個部分就是各種個性化的處理了,每個Prefab都大不相同。此處介紹一點技巧:inst:DoTaskInTime在初始化中的應用。

在很多Prefab的初始化函數(shù)里,都可以找到類似的語句: inst:DoTaskInTime(0, OnInit),這是為實體做特定的初始化,OnInit就是初始化函數(shù)。之所以會這樣寫,而不是直接執(zhí)行OnInit,是因為經(jīng)過DoTaskInTime的封裝,OnInit可以在生成Entity,完成初始化后再執(zhí)行。有人可能會說,把OnInit放在最后執(zhí)行就可以了。實際上并非如此,在執(zhí)行完初始化函數(shù)之后,還有一些后續(xù)處理,才算真正完成了初始化。這樣的操作可以保證某些參數(shù)設(shè)置絕對完整,比如說不會出現(xiàn)要調(diào)用的Component沒有添加的情況。如果有某些特別的初始化需求,可以仿照這樣的操作來進行。比如rabbit里的這個初始化函數(shù),在生成后會判斷是冬天還是夏天,如果是冬天,會等5秒后變?yōu)槎茫@個過程需要執(zhí)行一個等待5秒的task,如果在初始化函數(shù)中設(shè)置,可能會因為各種原因?qū)е聼o法執(zhí)行。

結(jié)語

prefab的主要目的就是提供生成entity的模板,其中最重要的部分是初始化函數(shù)。初始化函數(shù)中的代碼可以分為這幾個部分:創(chuàng)建實體、系統(tǒng)底層組件、tag、SG和Brain、Component、監(jiān)聽和其它設(shè)置。希望本篇文章能夠為各位同學在閱讀Prefab代碼時提供一些思路。下期會講解饑荒游戲中最核心的部分——組件,包括系統(tǒng)組件和Component。Prefab解釋了各種物體是如何構(gòu)建的,組件則為這些物體提供了豐富多樣的功能。

第二期 entity與prefab的評論 (共 條)

分享到微博請遵守國家法律
柘城县| 手机| 竹北市| 中江县| 皋兰县| 金溪县| 太仆寺旗| 利川市| 泰兴市| 安义县| 南开区| 东台市| 宁晋县| 依兰县| 延津县| 龙川县| 都安| 晋州市| 石门县| 两当县| 金湖县| 永年县| 大姚县| 惠安县| 开远市| 石门县| 汕尾市| 大洼县| 密山市| 安福县| 东安县| 临邑县| 蓬安县| 鹤岗市| 肇庆市| 安西县| 阆中市| 邓州市| 洛阳市| 沾化县| 德令哈市|