Tags: InsideUE4
UE4深入學習QQ群: 456247757
引言
前文提到說一個World管理多個Level,並負責它們的加載釋放。那么,問題來了,一個游戲里是只有一個World嗎?
WorldContext
答案是否定的,首先World就不是只有一種類型,比如編輯器本身就也是一個World,里面顯示的游戲場景也是一個World,這兩個World互相協作構成了我們的編輯體驗。然后點播放的時候,引擎又可以生成新的類型World來讓我們測試。簡單來說,UE其實是一個平行宇宙世界觀。
以下是一些世界類型:
namespace EWorldType
{
enum Type
{
None, // An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels
Game, // The game world
Editor, // A world being edited in the editor
PIE, // A Play In Editor world
Preview, // A preview world for an editor tool
Inactive // An editor world that was loaded but not currently being edited in the level editor
};
}
而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 FWorldContext
{
[...]
TEnumAsByte<EWorldType::Type> WorldType;
FSeamlessTravelHandler SeamlessTravelHandler;
FName ContextHandle;
/** URL to travel to for pending client connect */
FString TravelURL;
/** TravelType for pending client connects */
uint8 TravelType;
/** URL the last time we traveled */
UPROPERTY()
struct FURL LastURL;
/** last server we connected to (for "reconnect" command) */
UPROPERTY()
struct FURL LastRemoteURL;
}
這里的TravelURL和TravelType就是負責設定下一個Level的目標和轉換過程。
// Traveling from server to server.
UENUM()
enum ETravelType
{
/** Absolute URL. */
TRAVEL_Absolute,
/** Partial (carry name, reset server). */
TRAVEL_Partial,
/** Relative URL. */
TRAVEL_Relative,
TRAVEL_MAX,
};
void UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )
{
FWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);
// set TravelURL. Will be processed safely on the next tick in UGameEngine::Tick().
Context.TravelURL = NextURL;
Context.TravelType = InTravelType;
[...]
}
粗略的流程是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)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject))
{
FLatentActionManager& LatentManager = World->GetLatentActionManager();
if (LatentManager.FindExistingAction<FStreamLevelAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
FStreamLevelAction* NewAction = new FStreamLevelAction(true, LevelName, bMakeVisibleAfterLoad, bShouldBlockOnLoad, LatentInfo, World);
LatentManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, NewAction);
}
}
}
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變量來保存。至此,我們已經到了引擎的最根處:
//UnrealEngine\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp
ENGINE_API UEngine* GEngine = NULL;
GEngine可以說是一切開始的地方了。翻看引擎源碼,到處也可以看見從GEngine->出來的引用。
GamePlayStatics
既然我們在引擎內部C++層次已經有了訪問World操作Level的能力,那么在暴露出的藍圖系統里,UE為了我們的使用方便,也在Engine層次為我們提供了便利操作藍圖函數庫。
UCLASS ()
class UGameplayStatics : public UBlueprintFunctionLibrary
我們在藍圖里見到的GetPlayerController、SpawActor和OpenLevel等都是來至於這個類的接口。這個類比較簡單,相當於一個C++的靜態類,只為藍圖暴露提供了一些靜態方法。在想借鑒或者是查詢某個功能的實現時,此處往往會是一個入口。
總結
從結構上而言,我們已經來到了最根源的地方。GEngine仿佛就是一棵大樹的根,當我們拎起它的時候,也會帶出整個游戲世界的各個對象。但目前這些對象:Object->Actor+Component->Level->World->WorldContext->GameInstance->Engine,確實已經足夠表達UE游戲世界的各個部分。
那作為GamePlay部分而言,我們還有一個問題:UE是如何把在該對象結構上表達游戲邏輯的?
如果說:“程序=數據+算法”的話,那UE的GamePlay我們已經討論完了數據部分,而下篇我們將開始討論UE的游戲邏輯“算法”部分。
UE4深入學習QQ群: 456247757
個人原創,未經授權,謝絕轉載!