引言

上篇我們講到了UE在World之上,繼續(xù)抽象出了Player的概念,包含了本地的ULocalPlayer和網(wǎng)絡(luò)的UNetConnection,并以此創(chuàng)建出了World中的PlayerController,從而實(shí)現(xiàn)了不同的玩家模式策略。一路向上,依照設(shè)計(jì)里一個(gè)最樸素的原理:自己是無法創(chuàng)建管理自身的,所以Player也需要一個(gè)創(chuàng)建管理和存儲(chǔ)的地方。另一方面,上文提到Player固然可以負(fù)責(zé)一些跟玩家相關(guān)的業(yè)務(wù)邏輯,但是對(duì)于World之上協(xié)調(diào)管理的邏輯卻也仍然無處安放。
如果是有一定的游戲開發(fā)實(shí)戰(zhàn)經(jīng)驗(yàn)的朋友也一定能體會(huì)到,在自己開發(fā)的游戲中,往往除了我們上文提到的Player類,常常會(huì)創(chuàng)建一個(gè)Game類,比如BattleGame、WarGame或HappyGame等等。Game之前的名詞往往都是游戲的開發(fā)代號(hào)。這倒不是因?yàn)槲覀內(nèi)绱藷嶂詣?chuàng)建各種Manager類,而是確實(shí)需要一個(gè)大管家來干一些協(xié)調(diào)的活。一般的游戲引擎都只會(huì)暴露給你它自己引擎的管理類,如Director,Engine或Application之類的,但是卻不會(huì)主動(dòng)在Game類的創(chuàng)建管理上為你提供方便。游戲引擎的出現(xiàn),最開始其實(shí)只是因?yàn)橐恍┤税l(fā)現(xiàn)游戲做著做著,有一大部分功能是可以復(fù)用的,于是就把它抽離了出來方便做下一款游戲。在那個(gè)時(shí)候,人們對(duì)游戲還是處于開荒探索的階段,游戲引擎只是一大堆功能的復(fù)合體,就像叮當(dāng)貓的口袋一樣,互相比誰掏出的工具最強(qiáng)大。然而即使到了現(xiàn)代,絕大部分的引擎的思想?yún)s還停留在上個(gè)世紀(jì),仍然執(zhí)著于羅列Feature列表,卻忘了真正的游戲開發(fā)人員天天面對(duì)的游戲業(yè)務(wù)邏輯編寫,沒有思考在那方面如何也下一番功夫去幫助開發(fā)者。人們對(duì)比UE和其他游戲引擎時(shí),也會(huì)常常說出的一句話是:“別忘了Epic自己也是做游戲的”(虛幻競(jìng)技場(chǎng),戰(zhàn)爭(zhēng)機(jī)器,無盡之劍……)。從這一點(diǎn)也可以看出,UE很大的得益于Epic實(shí)戰(zhàn)游戲開發(fā)的反哺,這一方面Unity就有點(diǎn)吃虧了,沒有自己親自下手干臟活累活,就不懂得急人民群眾之所急。所以如果一個(gè)游戲引擎能把GamePlay也做好了,那就不止是口袋了,而是知你懂你的叮當(dāng)貓本身。

GameInstance

簡(jiǎn)單的事情就不用多講了,UE提供的方案是一以貫之的,為我們提供了一個(gè)GameInstance類。為了受益于UObject的反射創(chuàng)建能力,直接繼承于UObject,這樣就可以依據(jù)一個(gè)Class直接動(dòng)態(tài)創(chuàng)建出來具體的GameInstance子類。

我并不想羅列所有的接口,UGameInstance里的接口大概有4類:

  1. 引擎的初始化加載,Init和ShutDown等(在引擎流程章節(jié)會(huì)詳細(xì)敘述)
  2. Player的創(chuàng)建,如CreateLocalPlayer,GetLocalPlayers之類的。
  3. GameMode的重載修改,這是從4.14新增加進(jìn)來改進(jìn),本來你只能為特定的某個(gè)Map配置好GameModeClass,但是現(xiàn)在GameInstance允許你重載它的PreloadContentForURL、CreateGameModeForURL和OverrideGameModeClass方法來hook改變這一流程。
  4. OnlineSession的管理,這部分邏輯跟網(wǎng)絡(luò)的機(jī)制有關(guān)(到時(shí)候再詳細(xì)介紹),目前可以簡(jiǎn)單理解為有一個(gè)網(wǎng)絡(luò)會(huì)話的管理輔助控制類。

而GameInstance是在GameEngine里創(chuàng)建的(先不談UEditorEngine):

void UGameEngine::Init(IEngineLoop* InEngineLoop)
{ //[...] // Create game instance.  For GameEngine, this should be the only GameInstance that ever gets created. {
        FStringClassReference GameInstanceClassName = GetDefault<UGameMapsSettings>()->GameInstanceClass;
        UClass* GameInstanceClass = (GameInstanceClassName.IsValid() ? LoadObject<UClass>(NULL, *GameInstanceClassName.ToString()) : UGameInstance::StaticClass()); if (GameInstanceClass == nullptr)
        {
            UE_LOG(LogEngine, Error, TEXT("Unable to load GameInstance Class '%s'. Falling back to generic UGameInstance."), *GameInstanceClassName.ToString());
            GameInstanceClass = UGameInstance::StaticClass();
        }
        GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);
        GameInstance->InitializeStandalone();
    } //[...] } //在BaseEngine.ini或DefaultEngine.init里你可以配置GameInstanceClass [/Script/EngineSettings.GameMapsSettings]
GameInstanceClass=/Script/Engine.GameInstance

先從配置中取出GameInstanceClass,然后動(dòng)態(tài)創(chuàng)建,一目了然。

思考:GameInstance只有一個(gè)嗎?
一般而言,是的。對(duì)于我們自己開發(fā)的游戲而言,我們始終只需要關(guān)注自己的一畝三分地,那么你可以認(rèn)為你子類化的那個(gè)GameInstance就像個(gè)單件一樣,全局唯一只有一個(gè),從游戲的開始到結(jié)束。但既然是本系列文章的讀者,自然也是不甘于只了解這么多的。
正如把網(wǎng)絡(luò)連接也當(dāng)作Player這個(gè)概念一樣,我們此時(shí)也需要重新審視一下Game這個(gè)概念。什么是一個(gè)Game?對(duì)于玩家而言,Game就是從打開到關(guān)閉的這整個(gè)過程說展現(xiàn)的內(nèi)容。但是對(duì)于開發(fā)者來說,這個(gè)概念就需要擴(kuò)充一下了。假設(shè)有個(gè)引擎支持雙擊圖標(biāo)一下子開出4個(gè)窗口來讓4個(gè)玩家獨(dú)立運(yùn)行,你能說得清這是一個(gè)Game還是4個(gè)Game在運(yùn)行嗎?哪一種說法都能自圓其說,但關(guān)鍵是哪一種概念劃分能更好的讓我們管理組織結(jié)構(gòu)。因此針對(duì)這種情況,如果是這4個(gè)窗口一點(diǎn)都不互相關(guān)聯(lián),或者只是單獨(dú)的共用地圖資源,那么用4個(gè)Game的概念來管理就更為合適。如果這4個(gè)窗口里運(yùn)行的內(nèi)容,實(shí)際上只是在同一個(gè)關(guān)卡里本地對(duì)戰(zhàn),內(nèi)存里互相直接通信,那用一個(gè)Game加上4個(gè)Player的概念就會(huì)變得更合適。所以針對(duì)這點(diǎn),你可以把Game理解為就像進(jìn)程一樣,進(jìn)程可以在同一個(gè)exe上多開,Game也可以在同一份游戲資源上開出多個(gè)運(yùn)行實(shí)例;進(jìn)程之間可以互相通信協(xié)作,Game的不同實(shí)例也可以互相溝通,不管是內(nèi)存中直接在Engine的協(xié)調(diào)下完成,還是通過Socket通信。
另一方面,一般游戲引擎都只是服務(wù)于游戲本身,而對(duì)于其配套的各種編輯器就像是對(duì)待外來的打工者一樣,編輯器往往只負(fù)責(zé)最終輸出游戲資源。由于應(yīng)用場(chǎng)景的不同,編輯器的架構(gòu)也常常根據(jù)相應(yīng)平臺(tái)而定,五花八門,有用Qt,MFC,WPF等各種平臺(tái)UI框架。而對(duì)于另一些有大志向的引擎,比如Unity和UE,其編輯器就是采用引擎自繪的方案(其優(yōu)劣暫不分析,以后聊到UI框架再細(xì)說)。所以游戲引擎這個(gè)時(shí)候,就更加的拔高了一個(gè)層次,就不再只是個(gè)“游戲”引擎了,而是個(gè)“程序”引擎了。因此UE本身的這套框架不光要服務(wù)游戲,還要服務(wù)編輯器,甚至是另外一些輔助程序。所以,Game的概念也就擴(kuò)充到了更上層的“程序”,變得更廣義了。
言歸正傳,因?yàn)閁E的這套Editor自繪機(jī)制,還有PIE(PlayInEditor),進(jìn)程里其實(shí)是可以同時(shí)有多個(gè)GameInstance的,如正在編輯的EditorWorld所屬于的,和Play之后的World屬于的。我想,這也就是為何UE把它叫做GameInstance而不是簡(jiǎn)單的Game的含義,其名字中就隱含了多個(gè)Instance的深意。我們現(xiàn)在再次回顧一下(GamePlay架構(gòu)(三)WorldContext,GameInstance,Engine)最后的結(jié)構(gòu)圖,了解一下GameInstance又是被誰管理的:

當(dāng)初我們是以數(shù)據(jù)的視角,在考察WorldContext的從屬的時(shí)候討論過這個(gè)結(jié)構(gòu)?,F(xiàn)在以邏輯的角度,明白了GameInstance也會(huì)被上層的Engine實(shí)例出來多個(gè),就會(huì)有更深的理解了。
再擴(kuò)充一下,在Engine之下允許同時(shí)運(yùn)行多個(gè)GameInstance,還會(huì)有許多其他好處,就像操作系統(tǒng)允許一份資源運(yùn)行多個(gè)進(jìn)程實(shí)例一樣,Engine就可以站在更高的層次上管理協(xié)調(diào)多個(gè)Game,同時(shí)也能更加的深入到Game內(nèi)部去得到更多的優(yōu)化。比如未來要實(shí)現(xiàn)游戲本地的host多開并管理,或者在Server同時(shí)Host一個(gè)Map的多個(gè)實(shí)例(現(xiàn)在只能一個(gè)……還是有很多工作要做啊),這對(duì)于開發(fā)MMO網(wǎng)游是非常需要的功能,雖然目前UE在這一塊的具體工作還有些薄弱,但至少可擴(kuò)展的可能性是已經(jīng)保證了的(動(dòng)手能力強(qiáng)的高手可以在此基礎(chǔ)上定制)。一般而言,間接多一層,就多了一層的靈活性,所以很多引擎其實(shí)就是把Game和Engine揉在了一塊沒有為了GamePlay框架而分開。

思考:哪些邏輯應(yīng)該放在GameInstance?
第二個(gè)慣例的問題是,這一層應(yīng)該寫些什么邏輯。顧名思義,既然是作為游戲中全局唯一的長(zhǎng)者,我們就應(yīng)該給他全局的控制權(quán)。在邏輯層面,GameInstance往下看是:

  1. Worlds,Level的切換實(shí)際發(fā)生地是Engine,而GameInstance可以說是UE之神其下的唯一代言人,所以GameInstance也可以代之管理World的切換等。我們可以在GameInstance里實(shí)現(xiàn)各種邏輯最后調(diào)用Engine的OpenLevel等接口。
  2. Players,雖然一般來說我們直接控制Players的機(jī)會(huì)不多,都是配置好了就行。但要是到了需要的時(shí)候,GameInstance也實(shí)現(xiàn)了許多的接口可以讓你動(dòng)態(tài)的添加刪除Players。
  3. UI,UE的UI是另一套World之外的系統(tǒng),雖然同屬于Viewport的顯示之下,但是控制結(jié)構(gòu)跟Actor們并不一樣。所以我們常常會(huì)需要控制UI各種切換的業(yè)務(wù)邏輯,雖然在Widget的Graph里也可以寫些簡(jiǎn)單的切換,但是要想復(fù)用某些切換邏輯的時(shí)候,在特定的Wdiget里就不合適了,而GameMode一方面局限于Level,另一方面又只存在于Server;PlayerController也是會(huì)切換掉的,同時(shí)又只存在于World中,所以最后比較合適的就剩下GameInstance了,以后當(dāng)然有可能了可能會(huì)擴(kuò)展出個(gè)UI的業(yè)務(wù)邏輯Manger類,不過那是后話了。
  4. 全局的配置,也常常需要根據(jù)平臺(tái)改變一些游戲的配置,Execute一些ConsoleCommand,GameInstance也是這些命令的存放地。
  5. 游戲的額外第三方邏輯,如果你的游戲需要其他一些控制,比如自己寫的網(wǎng)絡(luò)通信、自定義的配置文件或者自己的一些程序算法,如果簡(jiǎn)單的話,GameInstance也可以一放,等復(fù)雜起來了,也可以把GameInstance當(dāng)作一個(gè)模塊容器,你可以在里面再擴(kuò)展出來其他的子邏輯模塊。當(dāng)然如果是插件的話,還是在自己的插件Module里面自行管理邏輯,然后把協(xié)調(diào)工作交給GameInstance來做。

而在數(shù)據(jù)層面上,我們層層上來,已經(jīng)有了針對(duì)一個(gè)Player的Contoller的PlayerState,也有了針對(duì)World的GameMode的GameState,到了更全局之上,自然的GameInstance就應(yīng)該存儲(chǔ)一些全局的狀態(tài)數(shù)據(jù)。所以你可以在GameInstance的成員變量中添加一些全局的狀態(tài),或者是那些想要在Level之外持續(xù)存在的對(duì)象。不過需要注意的一點(diǎn)是,GameInstance成員變量中最好只保存那些“臨時(shí)”的數(shù)據(jù),而對(duì)于那些想要持久序列化保存的數(shù)據(jù),我們就需要接下來的SaveGame了。把持久的數(shù)據(jù)直接放在SaveGame,用的時(shí)候直接讀取出來,之后再直接在其上更新,好處是只用維護(hù)一份,省得要保存的時(shí)候,還去想到底要選GameInstance的哪些成員變量中來保存,一開始就設(shè)計(jì)選好,以后就方便了。

SaveGame

UE連玩家存檔都幫你做了!得益于UObject的序列化機(jī)制,現(xiàn)在你只需要繼承于USaveGame,并添加你想要的那些屬性字段,然后這個(gè)結(jié)構(gòu)就可以序列化保存下來的。玩家存檔也是游戲中一個(gè)非常常見的功能,差的引擎一般就只提供給你讀寫文件的接口,好一點(diǎn)的會(huì)繼續(xù)給你一些序列化機(jī)制,而更好的則會(huì)服務(wù)得更加周到。UE為我們?cè)谒{(lán)圖里提供了SaveGame的統(tǒng)一接口,讓你只用關(guān)心想序列化的數(shù)據(jù)。
USaveGame其實(shí)就是為了提供給UE一個(gè)UObject對(duì)象,本身并不需要其他額外的控制,所以它的類是如此的簡(jiǎn)單以至于我能直接把它的全部聲明展示出來:

UCLASS(abstract, Blueprintable, BlueprintType) class ENGINE_API USaveGame : public UObject
{ /** * @see UGameplayStatics::CreateSaveGameObject
     * @see UGameplayStatics::SaveGameToSlot
     * @see UGameplayStatics::DoesSaveGameExist
     * @see UGameplayStatics::LoadGameFromSlot
     * @see UGameplayStatics::DeleteGameInSlot
     */ GENERATED_UCLASS_BODY()
};

而UGameplayStatics作為暴露給藍(lán)圖的接口實(shí)現(xiàn)部分,其內(nèi)部的實(shí)現(xiàn)是:

先在內(nèi)存中寫入一些SavegameFileVersion之類的控制文件頭,然后再序列化USaveGame對(duì)象,接著會(huì)找到ISaveGameSystem接口,最后交于真正的子類實(shí)現(xiàn)文件的保存。目前的默認(rèn)實(shí)現(xiàn)是FGenericSaveGameSystem,其內(nèi)部也只是轉(zhuǎn)發(fā)到直接的文件讀寫接口上去。但你也可以實(shí)現(xiàn)自己的SaveGameSystem,不管是寫文件或者是網(wǎng)絡(luò)傳輸,保存到不同的地方去?;蛘呤莾?nèi)部調(diào)用OnlineSubsystem的Storage接口,直接把玩家存檔保存到Steam云存儲(chǔ)中也可以。
因此可見,單單是玩家存檔這件邊角的小事,UE作為一個(gè)深受游戲開發(fā)淬煉過的引擎,為了方便自己,也同時(shí)造福我們廣大開發(fā)者,已經(jīng)實(shí)現(xiàn)了這么一套完善的機(jī)制。
關(guān)于存檔數(shù)據(jù)關(guān)聯(lián)的邏輯,再重復(fù)幾句,對(duì)于那些需要直接在全局處理的數(shù)據(jù)邏輯,也可以直接在SaveGame中寫方法來實(shí)現(xiàn)。比如實(shí)現(xiàn)AddCoin接口,對(duì)外隱藏實(shí)現(xiàn),對(duì)內(nèi)可以自定義附加一些邏輯。USaveGame可以看作是一個(gè)全局持久數(shù)據(jù)的業(yè)務(wù)邏輯類。跟GameInstance里的數(shù)據(jù)區(qū)分就是,GameInstance里面的是臨時(shí)的數(shù)據(jù),SaveGame里是持久的。清晰這一點(diǎn)區(qū)分,到時(shí)就不會(huì)糾結(jié)哪些屬性放在哪里,哪些方法實(shí)現(xiàn)在哪里了。
注意一下,SaveGameToSlot里的SlotName可以理解為存檔的文件名,UserIndex是用來標(biāo)識(shí)是哪個(gè)玩家在存檔。UserIndex是預(yù)留的,在目前的UE實(shí)現(xiàn)里并沒有用到,只是預(yù)留給一些平臺(tái)提供足夠的信息。你也可以利用這個(gè)信息來為多個(gè)不同玩家生成不同的最后文件名什么的。而ISaveGameSystem是IPlatformFeaturesModule提供的模塊接口,關(guān)于模塊的機(jī)制,等引擎流程章節(jié)再說吧,目前可以簡(jiǎn)單理解為一個(gè)單件對(duì)象里提供了一些平臺(tái)相關(guān)的接口對(duì)象。

總結(jié)

至此,我們可以說已經(jīng)介紹完了GamePlay下半部分——邏輯控制。在藍(lán)圖層,UE并不向BP直接暴露Engine概念,即使在C++層,在實(shí)現(xiàn)GamePlay業(yè)務(wù)時(shí)也是很少需要真正直接操縱Engine的時(shí)候。如果GamePlay已經(jīng)足夠好,那么Engine自然就可以隱居幕后了。UE用GameInstance實(shí)現(xiàn)了全局的控制,并支持多GameInstance來實(shí)現(xiàn)編輯器,最后在存檔的時(shí)候還可以用到SaveGame的方便的接口。
下篇,就是GamePlay章節(jié)的最終章,我們將會(huì)對(duì)GamePlay架構(gòu)的(一到九)篇進(jìn)行回顧歸納總結(jié)鞏固,以一個(gè)承上啟下總覽的眼光,再來重新審視一下UE的整套GamePlay框架,下個(gè)章節(jié)見。

引用

  1. SaveGame

UE4.14

作者的話:GamePlay架構(gòu)9篇下來,我也在探索不同書寫風(fēng)格,希望能夠?yàn)楹罄m(xù)的其他章節(jié)確定下來基調(diào)。對(duì)于文風(fēng)、內(nèi)容組織或其他問題,還請(qǐng)各位能直言批評(píng)指教(留言私信全都?xì)g迎)。目前也處在準(zhǔn)備下個(gè)大章節(jié)(UObject)的階段,也希望能有更多建議,多謝。