WorldContext

答案是否定的,首先World就不是只有一種類型,比如編輯器本身就也是一個(gè)World,里面顯示的游戲場(chǎng)景也是一個(gè)World,這兩個(gè)World互相協(xié)作構(gòu)成了我們的編輯體驗(yàn)。然后點(diǎn)播放的時(shí)候,引擎又可以生成新的類型World來讓我們測(cè)試。簡(jiǎn)單來說,UE其實(shí)是一個(gè)平行宇宙世界觀。
以下是一些世界類型:

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來指向當(dāng)前的World。而當(dāng)需要從一個(gè)World切換到另一個(gè)World的時(shí)候(比如說當(dāng)點(diǎn)擊播放時(shí),就是從Preview切換到PIE),F(xiàn)WorldContext就用來保存切換過程信息和目標(biāo)World上下文信息。所以一般在切換的時(shí)候,比如OpenLevel,也都會(huì)需要傳FWorldContext的參數(shù)。一般就來說,對(duì)于獨(dú)立運(yùn)行的游戲,WorldContext只有唯一個(gè)。而對(duì)于編輯器模式,則是一個(gè)WorldContext給編輯器,一個(gè)WorldContext給PIE(Play In Editor)的World。一般來說我們不需要直接操作到這個(gè)類,引擎內(nèi)部已經(jīng)處理好各種World的協(xié)作。
不僅如此,同時(shí)FWorldContext還保存著World里L(fēng)evel切換的上下文:

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就是負(fù)責(zé)設(shè)定下一個(gè)Level的目標(biāo)和轉(zhuǎn)換過程。

// 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的時(shí)候, 先設(shè)置當(dāng)前World的Context上的TravelURL,然后在UEngine::TickWorldTravel的時(shí)候判斷TravelURL非空來真正執(zhí)行Level的切換。具體的Level切換詳細(xì)流程比較復(fù)雜,目前先從大局上理解整體結(jié)構(gòu)??偠灾琖orldContext既負(fù)責(zé)World之間切換的上下文,也負(fù)責(zé)Level之間切換的操作信息。

思考:為何Level的切換信息不放在World里?
因?yàn)閁E有一個(gè)邏輯,一個(gè)World只有一個(gè)PersistentLevel(見上篇),而當(dāng)我們OpenLevel一個(gè)PersistentLevel的時(shí)候,實(shí)際上引擎做的是先釋放掉當(dāng)前的World,然后再創(chuàng)建個(gè)新的World。所以如果我們把下一個(gè)Level的信息放在當(dāng)前的World中,就不得不在釋放當(dāng)前World前又拷貝回來一遍了。
而LoadStreamLevel的時(shí)候,就只是在當(dāng)前的World中載入對(duì)象了,所以其實(shí)就沒有這個(gè)限制了。

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()其實(shí)也算是保存在當(dāng)前World里了。

思考:為何World和Level的切換要放在下一幀再執(zhí)行?
首先Level的加載顯然是比較慢的,需要載入Map,相應(yīng)的Mesh,Material……等等。所以這個(gè)操作就必須異步化,異步的話其實(shí)就剩下兩種方式,一種是先記錄下來信息之后再執(zhí)行;一種是命令模式立馬往隊(duì)列里壓個(gè)命令之后再執(zhí)行。注意,因?yàn)镺penLevel還要相應(yīng)在主線程生成相應(yīng)Actor對(duì)象,所以有些部分還是要在主線程完成的。這兩種模式其實(shí)都可以達(dá)成需求,前者更加簡(jiǎn)單明了,后者相對(duì)統(tǒng)一。UE也是個(gè)進(jìn)化過來的引擎,也并不是所有的代碼都完美無缺。猜想其實(shí)也是一開始這么簡(jiǎn)單就這么做了,后來也沒有特別大的改動(dòng)的動(dòng)力就一直這樣了。引擎最終比的是生產(chǎn)效率的提高,確實(shí)也不是代碼有多優(yōu)雅。

GameInstance

那么這些WorldContexts又是保存在哪里的呢?追根溯源:

GameInstance里會(huì)保存著當(dāng)前的WorldConext和其他整個(gè)游戲的信息。明白了GameInstance是比World更高的層次之后,我們也就能明白為何那些獨(dú)立于Level的邏輯或數(shù)據(jù)要在GameInstance中存儲(chǔ)了。
這一點(diǎn)其實(shí)也很好理解,大凡游戲引擎都會(huì)有一個(gè)Game的概念,不管是叫Application還是Director,它都是玩家能直接接觸到的最根源的操作類。而UE的GameInstance因?yàn)槔^承于UObject,所以就擁有了動(dòng)態(tài)創(chuàng)建的能力,所以我們可以通過指定GameInstanceClass來讓UE創(chuàng)建使用我們自定義的GameInstance子類。所以不論是C++還是BP,我們通常會(huì)繼承于GameInstance,然后在里面編寫應(yīng)用于整個(gè)游戲范圍的邏輯。
因?yàn)榻?jīng)常有初學(xué)者會(huì)問到:我的Level切換了,變量數(shù)據(jù)就丟了,我應(yīng)該把那些數(shù)據(jù)放在哪?再清晰直白一點(diǎn),GameInstance就是你不管Level怎么切換,還是會(huì)一直存在的那個(gè)對(duì)象!

Engine

讓我們繼續(xù)再往上,終于得見UE大神:

此處UEngine分化出了兩個(gè)子類:UGameEngine和UEditorEngine。眾所周知,UE的編輯器也是UE用自己的引擎渲染出來的,采用的也是Slate那套UI框架。好處有很多,比如跨平臺(tái)比較統(tǒng)一,UI框架可以復(fù)用一套控件庫(kù),Dogfood等等,此處不再細(xì)講。所以本質(zhì)上來說,UE的編輯器其實(shí)也是個(gè)游戲!我們是在編輯器這個(gè)游戲里面創(chuàng)造我們自己的另一個(gè)游戲。話雖如此,但比較編輯器和游戲還是有一定差別的,所以UE會(huì)在不同模式下根據(jù)編譯環(huán)境而采用不同的具體Engine類,而在基類UEngine里通過一個(gè)WorldList保存了所有的World。

  • Standlone Game:會(huì)使用UGameEngine來創(chuàng)建出唯一的一個(gè)GameWorld,因?yàn)橐仓挥幸粋€(gè),所以為了方便起見,就直接保存了GameInstance指針。
  • 而對(duì)于編輯器來說,EditorWorld其實(shí)只是用來預(yù)覽,所以并不擁有OwningGameInstance,而PlayWorld里的OwningGameInstance才是間接保存了GameInstance.

目前來說,因?yàn)閁E還不支持同時(shí)運(yùn)行多個(gè)World(當(dāng)前只能一個(gè),但可以切換),所以GameInstance其實(shí)也是唯一的。提前說些題外話,雖然目前網(wǎng)絡(luò)部分還沒涉及到,但是當(dāng)我們?cè)贓ditor里進(jìn)行MultiplePlayer的測(cè)試時(shí),每一個(gè)Player Window里都是一個(gè)World。如果是DedicateServer模式,那DedicateServer也會(huì)是一個(gè)World。
最后實(shí)例化出來的UEngine實(shí)例用一個(gè)全局的GEngine變量來保存。至此,我們已經(jīng)到了引擎的最根處:

//UnrealEngine\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp ENGINE_API UEngine* GEngine = NULL;

GEngine可以說是一切開始的地方了。翻看引擎源碼,到處也可以看見從GEngine->出來的引用。

GamePlayStatics

既然我們?cè)谝鎯?nèi)部C++層次已經(jīng)有了訪問World操作Level的能力,那么在暴露出的藍(lán)圖系統(tǒng)里,UE為了我們的使用方便,也在Engine層次為我們提供了便利操作藍(lán)圖函數(shù)庫(kù)。

UCLASS () class UGameplayStatics : public UBlueprintFunctionLibrary

我們?cè)谒{(lán)圖里見到的GetPlayerController、SpawActor和OpenLevel等都是來至于這個(gè)類的接口。這個(gè)類比較簡(jiǎn)單,相當(dāng)于一個(gè)C++的靜態(tài)類,只為藍(lán)圖暴露提供了一些靜態(tài)方法。在想借鑒或者是查詢某個(gè)功能的實(shí)現(xiàn)時(shí),此處往往會(huì)是一個(gè)入口。

總結(jié)

從結(jié)構(gòu)上而言,我們已經(jīng)來到了最根源的地方。GEngine仿佛就是一棵大樹的根,當(dāng)我們拎起它的時(shí)候,也會(huì)帶出整個(gè)游戲世界的各個(gè)對(duì)象。但目前這些對(duì)象:Object->Actor+Component->Level->World->WorldContext->GameInstance->Engine,確實(shí)已經(jīng)足夠表達(dá)UE游戲世界的各個(gè)部分。
那作為GamePlay部分而言,我們還有一個(gè)問題:UE是如何把在該對(duì)象結(jié)構(gòu)上表達(dá)游戲邏輯的?
如果說:“程序=數(shù)據(jù)+算法”的話,那UE的GamePlay我們已經(jīng)討論完了數(shù)據(jù)部分,而下篇我們將開始討論UE的游戲邏輯“算法”部分。