GameInstance這個類可以跨關卡存在,它不會因為切換關卡或者切換游戲模式而被銷毀。然而,GameMode和PlayController就會再切換關卡或者游戲模式時被引擎銷毀重置,這樣他們里面的狀態就不能被保存。比如,你想再下一個關卡中知道上一個關卡游戲角色的位置,這時就得在GameInstance中保存游戲角色在上一個關卡的位置。用戶登錄的賬號信息也可以保存在GameInstance中。每一個關卡都可以對應不同的GameMode和PlayController
引言
上篇我們講到了UE在World之上,繼續抽象出了Player的概念,包含了本地的ULocalPlayer和網絡的UNetConnection,並以此創建出了World中的PlayerController,從而實現了不同的玩家模式策略。一路向上,依照設計里一個最朴素的原理:自己是無法創建管理自身的,所以Player也需要一個創建管理和存儲的地方。另一方面,上文提到Player固然可以負責一些跟玩家相關的業務邏輯,但是對於World之上協調管理的邏輯卻也仍然無處安放。
如果是有一定的游戲開發實戰經驗的朋友也一定能體會到,在自己開發的游戲中,往往除了我們上文提到的Player類,常常會創建一個Game類,比如BattleGame、WarGame或HappyGame等等。Game之前的名詞往往都是游戲的開發代號。這倒不是因為我們如此熱衷創建各種Manager類,而是確實需要一個大管家來干一些協調的活。一般的游戲引擎都只會暴露給你它自己引擎的管理類,如Director,Engine或Application之類的,但是卻不會主動在Game類的創建管理上為你提供方便。游戲引擎的出現,最開始其實只是因為一些人發現游戲做着做着,有一大部分功能是可以復用的,於是就把它抽離了出來方便做下一款游戲。在那個時候,人們對游戲還是處於開荒探索的階段,游戲引擎只是一大堆功能的復合體,就像叮當貓的口袋一樣,互相比誰掏出的工具最強大。然而即使到了現代,絕大部分的引擎的思想卻還停留在上個世紀,仍然執着於羅列Feature列表,卻忘了真正的游戲開發人員天天面對的游戲業務邏輯編寫,沒有思考在那方面如何也下一番功夫去幫助開發者。人們對比UE和其他游戲引擎時,也會常常說出的一句話是:“別忘了Epic自己也是做游戲的”(虛幻競技場,戰爭機器,無盡之劍……)。從這一點也可以看出,UE很大的得益於Epic實戰游戲開發的反哺,這一方面Unity就有點吃虧了,沒有自己親自下手干臟活累活,就不懂得急人民群眾之所急。所以如果一個游戲引擎能把GamePlay也做好了,那就不止是口袋了,而是知你懂你的叮當貓本身。
GameInstance
簡單的事情就不用多講了,UE提供的方案是一以貫之的,為我們提供了一個GameInstance類。為了受益於UObject的反射創建能力,直接繼承於UObject,這樣就可以依據一個Class直接動態創建出來具體的GameInstance子類。
我並不想羅列所有的接口,UGameInstance里的接口大概有4類:
- 引擎的初始化加載,Init和ShutDown等(在引擎流程章節會詳細敘述)
- Player的創建,如CreateLocalPlayer,GetLocalPlayers之類的。
- GameMode的重載修改,這是從4.14新增加進來改進,本來你只能為特定的某個Map配置好GameModeClass,但是現在GameInstance允許你重載它的PreloadContentForURL、CreateGameModeForURL和OverrideGameModeClass方法來hook改變這一流程。
- OnlineSession的管理,這部分邏輯跟網絡的機制有關(到時候再詳細介紹),目前可以簡單理解為有一個網絡會話的管理輔助控制類。
而GameInstance是在GameEngine里創建的(先不談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,然后動態創建,一目了然。
思考:GameInstance只有一個嗎?
一般而言,是的。對於我們自己開發的游戲而言,我們始終只需要關注自己的一畝三分地,那么你可以認為你子類化的那個GameInstance就像個單件一樣,全局唯一只有一個,從游戲的開始到結束。但既然是本系列文章的讀者,自然也是不甘於只了解這么多的。
正如把網絡連接也當作Player這個概念一樣,我們此時也需要重新審視一下Game這個概念。什么是一個Game?對於玩家而言,Game就是從打開到關閉的這整個過程說展現的內容。但是對於開發者來說,這個概念就需要擴充一下了。假設有個引擎支持雙擊圖標一下子開出4個窗口來讓4個玩家獨立運行,你能說得清這是一個Game還是4個Game在運行嗎?哪一種說法都能自圓其說,但關鍵是哪一種概念划分能更好的讓我們管理組織結構。因此針對這種情況,如果是這4個窗口一點都不互相關聯,或者只是單獨的共用地圖資源,那么用4個Game的概念來管理就更為合適。如果這4個窗口里運行的內容,實際上只是在同一個關卡里本地對戰,內存里互相直接通信,那用一個Game加上4個Player的概念就會變得更合適。所以針對這點,你可以把Game理解為就像進程一樣,進程可以在同一個exe上多開,Game也可以在同一份游戲資源上開出多個運行實例;進程之間可以互相通信協作,Game的不同實例也可以互相溝通,不管是內存中直接在Engine的協調下完成,還是通過Socket通信。
另一方面,一般游戲引擎都只是服務於游戲本身,而對於其配套的各種編輯器就像是對待外來的打工者一樣,編輯器往往只負責最終輸出游戲資源。由於應用場景的不同,編輯器的架構也常常根據相應平台而定,五花八門,有用Qt,MFC,WPF等各種平台UI框架。而對於另一些有大志向的引擎,比如Unity和UE,其編輯器就是采用引擎自繪的方案(其優劣暫不分析,以后聊到UI框架再細說)。所以游戲引擎這個時候,就更加的拔高了一個層次,就不再只是個“游戲”引擎了,而是個“程序”引擎了。因此UE本身的這套框架不光要服務游戲,還要服務編輯器,甚至是另外一些輔助程序。所以,Game的概念也就擴充到了更上層的“程序”,變得更廣義了。
言歸正傳,因為UE的這套Editor自繪機制,還有PIE(PlayInEditor),進程里其實是可以同時有多個GameInstance的,如正在編輯的EditorWorld所屬於的,和Play之后的World屬於的。我想,這也就是為何UE把它叫做GameInstance而不是簡單的Game的含義,其名字中就隱含了多個Instance的深意。我們現在再次回顧一下(GamePlay架構(三)WorldContext,GameInstance,Engine)最后的結構圖,了解一下GameInstance又是被誰管理的:
當初我們是以數據的視角,在考察WorldContext的從屬的時候討論過這個結構。現在以邏輯的角度,明白了GameInstance也會被上層的Engine實例出來多個,就會有更深的理解了。
再擴充一下,在Engine之下允許同時運行多個GameInstance,還會有許多其他好處,就像操作系統允許一份資源運行多個進程實例一樣,Engine就可以站在更高的層次上管理協調多個Game,同時也能更加的深入到Game內部去得到更多的優化。比如未來要實現游戲本地的host多開並管理,或者在Server同時Host一個Map的多個實例(現在只能一個……還是有很多工作要做啊),這對於開發MMO網游是非常需要的功能,雖然目前UE在這一塊的具體工作還有些薄弱,但至少可擴展的可能性是已經保證了的(動手能力強的高手可以在此基礎上定制)。一般而言,間接多一層,就多了一層的靈活性,所以很多引擎其實就是把Game和Engine揉在了一塊沒有為了GamePlay框架而分開。
思考:哪些邏輯應該放在GameInstance?
第二個慣例的問題是,這一層應該寫些什么邏輯。顧名思義,既然是作為游戲中全局唯一的長者,我們就應該給他全局的控制權。在邏輯層面,GameInstance往下看是:
- Worlds,Level的切換實際發生地是Engine,而GameInstance可以說是UE之神其下的唯一代言人,所以GameInstance也可以代之管理World的切換等。我們可以在GameInstance里實現各種邏輯最后調用Engine的OpenLevel等接口。
- Players,雖然一般來說我們直接控制Players的機會不多,都是配置好了就行。但要是到了需要的時候,GameInstance也實現了許多的接口可以讓你動態的添加刪除Players。
- UI,UE的UI是另一套World之外的系統,雖然同屬於Viewport的顯示之下,但是控制結構跟Actor們並不一樣。所以我們常常會需要控制UI各種切換的業務邏輯,雖然在Widget的Graph里也可以寫些簡單的切換,但是要想復用某些切換邏輯的時候,在特定的Wdiget里就不合適了,而GameMode一方面局限於Level,另一方面又只存在於Server;PlayerController也是會切換掉的,同時又只存在於World中,所以最后比較合適的就剩下GameInstance了,以后當然有可能了可能會擴展出個UI的業務邏輯Manger類,不過那是后話了。
- 全局的配置,也常常需要根據平台改變一些游戲的配置,Execute一些ConsoleCommand,GameInstance也是這些命令的存放地。
- 游戲的額外第三方邏輯,如果你的游戲需要其他一些控制,比如自己寫的網絡通信、自定義的配置文件或者自己的一些程序算法,如果簡單的話,GameInstance也可以一放,等復雜起來了,也可以把GameInstance當作一個模塊容器,你可以在里面再擴展出來其他的子邏輯模塊。當然如果是插件的話,還是在自己的插件Module里面自行管理邏輯,然后把協調工作交給GameInstance來做。
而在數據層面上,我們層層上來,已經有了針對一個Player的Contoller的PlayerState,也有了針對World的GameMode的GameState,到了更全局之上,自然的GameInstance就應該存儲一些全局的狀態數據。所以你可以在GameInstance的成員變量中添加一些全局的狀態,或者是那些想要在Level之外持續存在的對象。不過需要注意的一點是,GameInstance成員變量中最好只保存那些“臨時”的數據,而對於那些想要持久序列化保存的數據,我們就需要接下來的SaveGame了。把持久的數據直接放在SaveGame,用的時候直接讀取出來,之后再直接在其上更新,好處是只用維護一份,省得要保存的時候,還去想到底要選GameInstance的哪些成員變量中來保存,一開始就設計選好,以后就方便了。
SaveGame
UE連玩家存檔都幫你做了!得益於UObject的序列化機制,現在你只需要繼承於USaveGame,並添加你想要的那些屬性字段,然后這個結構就可以序列化保存下來的。玩家存檔也是游戲中一個非常常見的功能,差的引擎一般就只提供給你讀寫文件的接口,好一點的會繼續給你一些序列化機制,而更好的則會服務得更加周到。UE為我們在藍圖里提供了SaveGame的統一接口,讓你只用關心想序列化的數據。
USaveGame其實就是為了提供給UE一個UObject對象,本身並不需要其他額外的控制,所以它的類是如此的簡單以至於我能直接把它的全部聲明展示出來:
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作為暴露給藍圖的接口實現部分,其內部的實現是:
先在內存中寫入一些SavegameFileVersion之類的控制文件頭,然后再序列化USaveGame對象,接着會找到ISaveGameSystem接口,最后交於真正的子類實現文件的保存。目前的默認實現是FGenericSaveGameSystem,其內部也只是轉發到直接的文件讀寫接口上去。但你也可以實現自己的SaveGameSystem,不管是寫文件或者是網絡傳輸,保存到不同的地方去。或者是內部調用OnlineSubsystem的Storage接口,直接把玩家存檔保存到Steam雲存儲中也可以。
因此可見,單單是玩家存檔這件邊角的小事,UE作為一個深受游戲開發淬煉過的引擎,為了方便自己,也同時造福我們廣大開發者,已經實現了這么一套完善的機制。
關於存檔數據關聯的邏輯,再重復幾句,對於那些需要直接在全局處理的數據邏輯,也可以直接在SaveGame中寫方法來實現。比如實現AddCoin接口,對外隱藏實現,對內可以自定義附加一些邏輯。USaveGame可以看作是一個全局持久數據的業務邏輯類。跟GameInstance里的數據區分就是,GameInstance里面的是臨時的數據,SaveGame里是持久的。清晰這一點區分,到時就不會糾結哪些屬性放在哪里,哪些方法實現在哪里了。
注意一下,SaveGameToSlot里的SlotName可以理解為存檔的文件名,UserIndex是用來標識是哪個玩家在存檔。UserIndex是預留的,在目前的UE實現里並沒有用到,只是預留給一些平台提供足夠的信息。你也可以利用這個信息來為多個不同玩家生成不同的最后文件名什么的。而ISaveGameSystem是IPlatformFeaturesModule提供的模塊接口,關於模塊的機制,等引擎流程章節再說吧,目前可以簡單理解為一個單件對象里提供了一些平台相關的接口對象。
總結
至此,我們可以說已經介紹完了GamePlay下半部分——邏輯控制。在藍圖層,UE並不向BP直接暴露Engine概念,即使在C++層,在實現GamePlay業務時也是很少需要真正直接操縱Engine的時候。如果GamePlay已經足夠好,那么Engine自然就可以隱居幕后了。UE用GameInstance實現了全局的控制,並支持多GameInstance來實現編輯器,最后在存檔的時候還可以用到SaveGame的方便的接口。
下篇,就是GamePlay章節的最終章,我們將會對GamePlay架構的(一到九)篇進行回顧歸納總結鞏固,以一個承上啟下總覽的眼光,再來重新審視一下UE的整套GamePlay框架,下個章節見。
引用
UE4.14
作者的話:GamePlay架構9篇下來,我也在探索不同書寫風格,希望能夠為后續的其他章節確定下來基調。對於文風、內容組織或其他問題,還請各位能直言批評指教(留言私信全都歡迎)。目前也處在准備下個大章節(UObject)的階段,也希望能有更多建議,多謝。
轉載自:https://www.cnblogs.com/fjz13/p/6109330.html