《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:

FWorldContext保存著ThisCurrentWorld來指向當前的World。而當需要從一個World切換到另一個World的時候(比如說當點擊播放時,就是從Preview切換到PIE),FWorldContext就用來保存切換過程信息和目標World上下文信息。所以一般在切換的時候,比如OpenLevel,也都會需要傳FWorldContext的參數。一般就來說,對於獨立運行的遊戲,WorldContext只有唯一個。而對於編輯器模式,則是一個WorldContext給編輯器,一個WorldContext給PIE(Play In Editor)的World。一般來說我們不需要直接操作到這個類,引擎內部已經處理好各種World的協作。

不僅如此,同時FWorldContext還保存著World里Level切換的上下文:

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?
從零開始手敲次世代遊戲引擎(十四)

TAG:游戏引擎 | 虚幻引擎 | 游戏开发 |