《InsideUE4》GamePlay架構(三)WorldContext,GameInstance,Engine
引言
前文提到說一個World管理多個Level,並負責它們的載入釋放。那麼,問題來了,一個遊戲里是只有一個World嗎?
WorldContext
答案是否定的,首先World就不是只有一種類型,比如編輯器本身就也是一個World,裡面顯示的遊戲場景也是一個World,這兩個World互相協作構成了我們的編輯體驗。然後點播放的時候,引擎又可以生成新的類型World來讓我們測試。簡單來說,UE其實是一個平行宇宙世界觀。
以下是一些世界類型:namespace EWorldTypen{ntenum Typent{nttNone,tt// An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levelsnttGame,tt// The game worldnttEditor,tt// A world being edited in the editornttPIE,tt// A Play In Editor worldnttPreview,t// A preview world for an editor toolnttInactivet// An editor world that was loaded but not currently being edited in the level editornt};n}n
而UE用來管理和跟蹤這些World的工具就是WorldContext:
struct FWorldContextn{n [...]ntTEnumAsByte<EWorldType::Type>tWorldType;nntFSeamlessTravelHandler SeamlessTravelHandler;nntFName ContextHandle;nnt/** URL to travel to for pending client connect */ntFString TravelURL;nnt/** TravelType for pending client connects */ntuint8 TravelType;nnt/** URL the last time we traveled */ntUPROPERTY()ntstruct FURL LastURL;nnt/** last server we connected to (for "reconnect" command) */ntUPROPERTY()ntstruct FURL LastRemoteURL;nn}n
這裡的TravelURL和TravelType就是負責設定下一個Level的目標和轉換過程。
// Traveling from server to server.nUENUM()nenum ETravelTypen{nt/** Absolute URL. */ntTRAVEL_Absolute,nt/** Partial (carry name, reset server). */ntTRAVEL_Partial,nt/** Relative URL. */ntTRAVEL_Relative,ntTRAVEL_MAX,n};nnvoid UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )n{ntFWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);nt// set TravelURL. Will be processed safely on the next tick in UGameEngine::Tick().ntContext.TravelURL = NextURL;ntContext.TravelType = InTravelType;n [...]n}n
粗略的流程是UE在OpenLevel的時候, 先設置當前World的Context上的TravelURL,然後在UEngine::TickWorldTravel的時候判斷TravelURL非空來真正執行Level的切換。具體的Level切換詳細流程比較複雜,目前先從大局上理解整體結構。總而言之,WorldContext既負責World之間切換的上下文,也負責Level之間切換的操作信息。
思考:為何Level的切換信息不放在World里?
因為UE有一個邏輯,一個World只有一個PersistentLevel(見上篇),而當我們OpenLevel一個PersistentLevel的時候,實際上引擎做的是先釋放掉當前的World,然後再創建個新的World。所以如果我們把下一個Level的信息放在當前的World中,就不得不在釋放當前World前又拷貝回來一遍了。 而LoadStreamLevel的時候,就只是在當前的World中載入對象了,所以其實就沒有這個限制了。void UGameplayStatics::LoadStreamLevel(UObject* WorldContextObject, FName LevelName,bool bMakeVisibleAfterLoad,bool bShouldBlockOnLoad,FLatentActionInfo LatentInfo)n{ntif (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject))nt{nttFLatentActionManager& LatentManager = World->GetLatentActionManager();nttif (LatentManager.FindExistingAction<FStreamLevelAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)ntt{ntttFStreamLevelAction* NewAction = new FStreamLevelAction(true, LevelName, bMakeVisibleAfterLoad, bShouldBlockOnLoad, LatentInfo, World);ntttLatentManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, NewAction);ntt}nt}n}n
World->GetLatentActionManager()其實也算是保存在當前World里了。
思考:為何World和Level的切換要放在下一幀再執行?
首先Level的載入顯然是比較慢的,需要載入Map,相應的Mesh,Material……等等。所以這個操作就必須非同步化,非同步的話其實就剩下兩種方式,一種是先記錄下來信息之後再執行;一種是命令模式立馬往隊列里壓個命令之後再執行。注意,因為OpenLevel還要相應在主線程生成相應Actor對象,所以有些部分還是要在主線程完成的。這兩種模式其實都可以達成需求,前者更加簡單明了,後者相對統一。UE也是個進化過來的引擎,也並不是所有的代碼都完美無缺。猜想其實也是一開始這麼簡單就這麼做了,後來也沒有特別大的改動的動力就一直這樣了。引擎最終比的是生產效率的提高,確實也不是代碼有多優雅。
GameInstance
那麼這些WorldContexts又是保存在哪裡的呢?追根溯源:
GameInstance里會保存著當前的WorldConext和其他整個遊戲的信息。明白了GameInstance是比World更高的層次之後,我們也就能明白為何那些獨立於Level的邏輯或數據要在GameInstance中存儲了。 這一點其實也很好理解,大凡遊戲引擎都會有一個Game的概念,不管是叫Application還是Director,它都是玩家能直接接觸到的最根源的操作類。而UE的GameInstance因為繼承於UObject,所以就擁有了動態創建的能力,所以我們可以通過指定GameInstanceClass來讓UE創建使用我們自定義的GameInstance子類。所以不論是C++還是BP,我們通常會繼承於GameInstance,然後在裡面編寫應用於整個遊戲範圍的邏輯。 因為經常有初學者會問到:我的Level切換了,變數數據就丟了,我應該把那些數據放在哪?再清晰直白一點,GameInstance就是你不管Level怎麼切換,還是會一直存在的那個對象!Engine
讓我們繼續再往上,終於得見UE大神:
此處UEngine分化出了兩個子類:UGameEngine和UEditorEngine。眾所周知,UE的編輯器也是UE用自己的引擎渲染出來的,採用的也是Slate那套UI框架。好處有很多,比如跨平台比較統一,UI框架可以復用一套控制項庫,Dogfood等等,此處不再細講。所以本質上來說,UE的編輯器其實也是個遊戲!我們是在編輯器這個遊戲裡面創造我們自己的另一個遊戲。話雖如此,但比較編輯器和遊戲還是有一定差別的,所以UE會在不同模式下根據編譯環境而採用不同的具體Engine類,而在基類UEngine里通過一個WorldList保存了所有的World。- Standlone Game:會使用UGameEngine來創建出唯一的一個GameWorld,因為也只有一個,所以為了方便起見,就直接保存了GameInstance指針。
- 而對於編輯器來說,EditorWorld其實只是用來預覽,所以並不擁有OwningGameInstance,而PlayWorld里的OwningGameInstance才是間接保存了GameInstance.
目前來說,因為UE還不支持同時運行多個World(當前只能一個,但可以切換),所以GameInstance其實也是唯一的。提前說些題外話,雖然目前網路部分還沒涉及到,但是當我們在Editor里進行MultiplePlayer的測試時,每一個Player Window里都是一個World。如果是DedicateServer模式,那DedicateServer也會是一個World。
最後實例化出來的UEngine實例用一個全局的GEngine變數來保存。至此,我們已經到了引擎的最根處:
//UnrealEngineEngineSourceRuntimeEnginePrivateUnrealEngine.cppnENGINE_API UEngine*tGEngine = NULL;n
GEngine可以說是一切開始的地方了。翻看引擎源碼,到處也可以看見從GEngine->出來的引用。
GamePlayStatics
既然我們在引擎內部C++層次已經有了訪問World操作Level的能力,那麼在暴露出的藍圖系統里,UE為了我們的使用方便,也在Engine層次為我們提供了便利操作藍圖函數庫。
UCLASS ()nclass UGameplayStatics : public UBlueprintFunctionLibrary n
我們在藍圖裡見到的GetPlayerController、SpawActor和OpenLevel等都是來至於這個類的介面。這個類比較簡單,相當於一個C++的靜態類,只為藍圖暴露提供了一些靜態方法。在想借鑒或者是查詢某個功能的實現時,此處往往會是一個入口。
總結
從結構上而言,我們已經來到了最根源的地方。GEngine彷彿就是一棵大樹的根,當我們拎起它的時候,也會帶出整個遊戲世界的各個對象。但目前這些對象:Object->Actor+Component->Level->World->WorldContext->GameInstance->Engine,確實已經足夠表達UE遊戲世界的各個部分。
那作為GamePlay部分而言,我們還有一個問題:UE是如何把在該對象結構上表達遊戲邏輯的? 如果說:「程序=數據+演算法」的話,那UE的GamePlay我們已經討論完了數據部分,而下篇我們將開始討論UE的遊戲邏輯「演算法」部分。上篇:《InsideUE4》GamePlay架構(二)Level和World
下篇:《InsideUE4》GamePlay架構(四)Pawn
UE4.14
---------------------------------------------------------------------------------------------------------------------------
知乎專欄:InsideUE4
UE4深入學習QQ群:456247757(非新手入門群,請先學習完官方文檔和視頻教程)
微信公眾號:aboutue,關於UE的一切新聞資訊、技巧問答、文章發布,歡迎關注。
個人原創,未經授權,謝絕轉載!
推薦閱讀:
※【內部分享】卡片怪獸伺服器框架從入門到放棄by 瑪尼
※遊戲開發和遊戲腳本的那些事
※為什麼cocos2dx不封裝一套開源成熟的伺服器端的框架?
※如何學習寫遊戲AI?
※從零開始手敲次世代遊戲引擎(十四)