引言

上文談到Actor和Component的關(guān)系,UE利用Actor的概念組成一片游戲?qū)ο笊郑⒗肅omponent組裝擴(kuò)展Actor的能力,讓世界里擁有了形形色色的Actor們,擁有了自由表達(dá)3D世界的能力。 
那么,這些Actor們,到底是怎么組織起來的呢?

既然提到了世界,我們的直覺反應(yīng)是采用一個(gè)"World"對(duì)象來包容所有的Actor們。但是當(dāng)游戲的虛擬世界非常巨大時(shí),這種方式就捉襟見肘了。首先,目前雖然PC的性能日益強(qiáng)大,但是依然內(nèi)存也限制了不能一下子加載進(jìn)所有的游戲資源;其次,因?yàn)橥婕业幕顒?dòng)和可見范圍有限,為了最優(yōu)性能,把即使是很遠(yuǎn)的跟玩家無關(guān)的對(duì)象也考慮進(jìn)來也明顯是不明智的。所以我們需要一種更細(xì)粒度的概念來劃分世界。 
不同的游戲引擎?zhèn)?,看待這個(gè)過程的角度和理念也不一樣。Cocos2dx會(huì)認(rèn)為游戲世界是由Scene組成的,Scene再由一個(gè)個(gè)Layer層疊表現(xiàn),然后再有一個(gè)Director來導(dǎo)演整個(gè)游戲。Unity覺得世界也是由Scene組成的,然后一個(gè)Application來扮演上帝來LoadLevel,后來換成了SceneManager。其他的,有的會(huì)稱為關(guān)卡(Level)或地圖(map)等等。而UE中把這種拆分叫做關(guān)卡(Level),由一個(gè)或多個(gè)Level組成一個(gè)World。 
不要覺得這種劃分好像很隨意,只是個(gè)名字不同而已。實(shí)際上一個(gè)游戲引擎的“世界觀”關(guān)系到了一整串后續(xù)的內(nèi)容組織,玩家的管理,世界的生成,變換和毀滅。游戲引擎內(nèi)部的資源的加載釋放也往往都是和這種劃分(Level)綁定在一起的。

 

Level

在UE的世界中,我們之前已經(jīng)有了空氣(C++),土壤(UObject),物件(Actor)。而現(xiàn)在UE又施展神力創(chuàng)建了一片片大陸(Level),在這片大陸上(.map文件),Actor們秩序井然,各種地形拔地而起,植被繁茂,天空霧云繚繞,圣光普照,這也是玩家們降生開始精彩冒險(xiǎn)的地方。 

可以從ULevel的前綴U看出來Level(大陸)也確實(shí)是繼承于UObject(土壤)的。那既然同屬于Object下面的各Actor們都擁有了一定的智能能力(支持藍(lán)圖腳本),Level自然也得體現(xiàn)出大地的意志,所以默認(rèn)帶了一個(gè)土地公(ALevelScriptActor),允許我們?cè)陉P(guān)卡里編寫腳本,可以對(duì)本關(guān)卡里的所有Actor通過名字呼之則來,關(guān)卡藍(lán)圖實(shí)際上就代表著該片大陸上的運(yùn)行規(guī)則。 
在Level已經(jīng)有了管理者之后,一開始大家都挺滿意,但漸漸的就發(fā)現(xiàn),好像各個(gè)Level需要的功能好像都差不多,都是修改一下光照,物理等一些屬性。所以為了方便起見,UE便給每一個(gè)Level也都默認(rèn)配了一個(gè)書記官(Info),他一一記錄著本Level的各種規(guī)則屬性,在UE需要的時(shí)候便負(fù)責(zé)相告。更重要的是,在Level需要有其他管理人員一起協(xié)助的時(shí)候,他也記錄著“游戲模式”的名字來讓UE可以指派。 
前面我們說過,有一些Actor是不“顯示”的(沒有SceneComponent),是不能“擺放”到Level里的,但是它依然可以在關(guān)卡里出力。其中一個(gè)家族系列就是AInfo和其之類。今天我們只簡(jiǎn)單介紹一下跟Level直接相關(guān)的一位書記官:AWorldSettings。 

其實(shí)雖然名字叫做WorldSettings,但其實(shí)只是跟Level相關(guān),我猜可能是在上古時(shí)代,當(dāng)時(shí)整個(gè)世界只有一塊大陸,人們就以為當(dāng)前的大陸就是整個(gè)世界,所以給這塊大陸的設(shè)置就起名為WorldSettings,后來等技術(shù)進(jìn)步了,發(fā)現(xiàn)必須有其他大陸了,這個(gè)名字已經(jīng)用得太多反而不好改了,就只好遺留下來了。當(dāng)然也有可能是因?yàn)楫?dāng)Level被添加進(jìn)World后,這個(gè)Level的Settings如果是主PersisitentLevel,那它就會(huì)被當(dāng)作整個(gè)World的WorldSettings。 
注意,Actors里也保存著AWorldSettings和ALevelScriptActor的指針,所以Actors實(shí)際上確實(shí)是保存了所有Actor。

思考:為何AWorldSettings要放進(jìn)在Actors[0]的位置?而ALevelScriptActor卻不用?

 

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 1 void ULevel::SortActorList() 2 { 3     if (Actors.Num() == 0) 4     { 5         // No need to sort an empty list 6         return; 7     } 8  9     TArray<AActor*> NewActors;10     TArray<AActor*> NewNetActors;11     NewActors.Reserve(Actors.Num());12     NewNetActors.Reserve(Actors.Num());13 14     check(WorldSettings);15 16     // The WorldSettings tries to stay at index 017     NewActors.Add(WorldSettings);18 19     // Add non-net actors to the NewActors immediately, cache off the net actors to Append after20     for (AActor* Actor : Actors)21     {22         if (Actor != nullptr && Actor != WorldSettings && !Actor->IsPendingKill())23         {24             if (IsNetActor(Actor))25             {26                 NewNetActors.Add(Actor);27             }28             else29             {30                 NewActors.Add(Actor);31             }32         }33 34     }35     iFirstNetRelevantActor = NewActors.Num();36 37     NewActors.Append(MoveTemp(NewNetActors));38 39     // Replace with sorted list.40     Actors = MoveTemp(NewActors);41 42     // Add all network actors to the owning world43     if ( OwningWorld != nullptr )44     {45         // Don't use sorted optimization outside of gameplay so we can safely shuffle around actors e.g. in the Editor46         // without there being a chance to break code using dynamic/ net relevant actor iterators.47         if (!OwningWorld->IsGameWorld())48         {49             iFirstNetRelevantActor = 0;50         }51 52         for ( int32 i = iFirstNetRelevantActor; i < Actors.Num(); i++ )53         {54             if ( Actors[ i ] != nullptr )55             {56                 OwningWorld->AddNetworkActor( Actors[ i ] );57             }58         }59     }60 }

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 

實(shí)際上通過這一段代碼可知,Actors們的排序依據(jù)是把那些“非網(wǎng)絡(luò)”的Actor放在前面,而把“網(wǎng)絡(luò)可復(fù)制”的Actor們放在后面,然后加一個(gè)起始索引標(biāo)記iFirstNetRelevantActor,相當(dāng)于為網(wǎng)絡(luò)Actor劃分了一個(gè)緩存,從而加速了網(wǎng)絡(luò)復(fù)制時(shí)的檢測(cè)速度。AWorldSettings因?yàn)槎际庆o態(tài)的數(shù)據(jù)提供者,在游戲運(yùn)行過程中也不會(huì)改變,不需要網(wǎng)絡(luò)復(fù)制,所以也就可以一直放在前列,而如果再加個(gè)規(guī)則,一直放在第一個(gè)的話,也能同時(shí)把AWorldSettings和其他的前列Actor們?cè)俣葏^(qū)分開,在需要的時(shí)候也能加速判斷。ALevelScriptActor因?yàn)槭谴黻P(guān)卡藍(lán)圖,是允許攜帶“復(fù)制”變量函數(shù)的,所以也有可能被排序到后列。

思考:既然ALevelScriptActor也繼承于AActor,為何關(guān)卡藍(lán)圖不設(shè)計(jì)能添加Component? 
觀察到,平常我們?cè)趧?chuàng)建Actor的時(shí)候,我們藍(lán)圖界面是可以創(chuàng)建Component的。 
那為什么在關(guān)卡藍(lán)圖里,卻不能這么做(沒有提供該界面功能)? 
我雖然在圖里標(biāo)出了Level中擁有ModelComponents,但那其實(shí)只是針對(duì)BSP應(yīng)用的一個(gè)子集。通過源碼發(fā)現(xiàn),其實(shí)UE自己也是在C++里往ALevelScriptActor添加UInputComponent來實(shí)現(xiàn)關(guān)卡藍(lán)圖可以響應(yīng)事件。

 

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 1 void ALevelScriptActor::PreInitializeComponents() 2 { 3     if (UInputDelegateBinding::SupportsInputDelegate(GetClass())) 4     { 5         // create an InputComponent object so that the level script actor can bind key events 6         InputComponent = NewObject<UInputComponent>(this); 7         InputComponent->RegisterComponent(); 8  9         UInputDelegateBinding::BindInputDelegates(GetClass(), InputComponent);10     }11     Super::PreInitializeComponents();12 }

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 

其實(shí)既然ALevelScriptActor是個(gè)Actor,那意味著我們當(dāng)然可以為它添加組件,實(shí)際上也確實(shí)可以這么做。比如你可以在關(guān)卡藍(lán)圖里這么干: 

而如果你實(shí)際意識(shí)到關(guān)卡藍(lán)圖本身就是一個(gè)看不見的Actor,你就可以在上面用Actor的各種操作: 
 
在關(guān)卡藍(lán)圖里的self其實(shí)也是個(gè)Actor!雖然一般這么干也沒什么毛用。 
那么好好想想,為啥UE要給你這么一個(gè)關(guān)卡藍(lán)圖界面呢? 
 
在此,我也只能進(jìn)行一番猜測(cè),ALevelScriptActor作為一個(gè)特化的Actor,卻把Components列表界面給隱藏了,說明UE其實(shí)是不希望我們?nèi)?fù)雜化關(guān)卡構(gòu)成的。 
假設(shè)說UE開放了關(guān)卡Component,那么我們?cè)趧?chuàng)建組件時(shí)就必然要考慮一個(gè)問題:哪些是ActorComponent,哪些是LevelComponent,再怎么ALevelScriptActor本質(zhì)是個(gè)Actor,但Level的概念還是要突出,ALevelScriptActor的Actor本質(zhì)是要隱藏的。所以用戶就會(huì)多一些心智負(fù)擔(dān),可能混淆。而如果像這樣不開放,大家的思路就都轉(zhuǎn)向先創(chuàng)建個(gè)Actor,然后再往之上添加component,思路會(huì)比較統(tǒng)一清晰。 
再之,從游戲邏輯的組織上來說,Level其實(shí)更應(yīng)該表現(xiàn)為一個(gè)Actor的容器。UE其實(shí)也是不鼓勵(lì)在Level里編寫太復(fù)雜的邏輯的。所以才接著會(huì)有了之后的GameMode,Controller那些真正的邏輯控制類(后續(xù)會(huì)再細(xì)討論)。 
所以游戲引擎也并不是說最大化的暴露一切功能給你就是最好的,有時(shí)候選擇太多了反而容易出錯(cuò)。在這一點(diǎn)上,我覺得UE很好的保持了克制,為我們提供了一個(gè)優(yōu)秀的清晰的不易出錯(cuò)的框架,同時(shí)也對(duì)高階用戶保留了靈活性。

 

World

終于,到了把大陸們(Level)拼裝起來的時(shí)候了。可以用SubLevel的方式: 

也支持WorldComposition的方式自動(dòng)把項(xiàng)目里的所有Level都組合起來,并設(shè)置擺放位置: 

具體擺放的操作和技巧并不是本文的重點(diǎn)。簡(jiǎn)單本質(zhì)來說,就是一個(gè)World里有多個(gè)Level,這些Level在什么位置,是在一開始就加載進(jìn)來,還是Streaming運(yùn)行時(shí)加載。 
UE里每個(gè)World支持一個(gè)PersisitentLevel和多個(gè)其他Level: 

Persisitent的意思是一開始就加載進(jìn)World,Streaming是后續(xù)動(dòng)態(tài)加載的意思。Levels里保存有所有的當(dāng)前已經(jīng)加載的Level,StreamingLevels保存整個(gè)World的Levels配置列表。PersisitentLevel和CurrentLevel只是個(gè)快速引用。在編輯器里編輯的時(shí)候,CurrentLevel可以指向其他Level,但運(yùn)行時(shí)CurrentLevel只能是指向PersisitentLevel。

思考:為何要有主PersisitentLevel? 
首先,World至少得有一個(gè)Level,就像你也得先出生在一塊大陸上才可以繼續(xù)談起去探索別的新大陸。所以這塊玩家出生的大陸就是主Level了。當(dāng)然了,因?yàn)槲覀円部梢酝瑫r(shí)配置別的Level一開始就加載進(jìn)來,其實(shí)跟PersisitentLevel是差不多等價(jià)的,但再考慮到另一問題:Levels拼接進(jìn)World一起之后,各自有各自的worldsetting,那整個(gè)World的配置應(yīng)該以誰的為主?

 

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

AWorldSettings* UWorld::GetWorldSettings( bool bCheckStreamingPesistent, bool bChecked ) const{
    checkSlow(IsInGameThread());
    AWorldSettings* WorldSettings = nullptr;    if (PersistentLevel)
    {
        WorldSettings = PersistentLevel->GetWorldSettings(bChecked);        if( bCheckStreamingPesistent )
        {            if( StreamingLevels.Num() > 0 &&
                StreamingLevels[0] &&
                StreamingLevels[0]->IsA<ULevelStreamingPersistent>()) 
            {
                ULevel* Level = StreamingLevels[0]->GetLoadedLevel();                if (Level != nullptr)
                {
                    WorldSettings = Level->GetWorldSettings();
                }
            }
        }
    }    return WorldSettings;
}

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 

可以看出,World的Settings也是以PersisitentLevel為主的,但這也并不以為著其他Level的Settings就完全沒有作用了,本篇也無法一一列出所有配置選項(xiàng)來說明,簡(jiǎn)單來說,就是需要在整個(gè)世界范圍內(nèi)起作用的配置選項(xiàng)(比如VR的WorldToMeters,KillZ,WorldGravity其他大部分都是)就是需要從主PersisitentLevel的配置中提取。而一些配置選項(xiàng)可以在單獨(dú)Level中起作用的,比如在編輯Level時(shí)的光照質(zhì)量配置就是一個(gè)個(gè)Level單獨(dú)的,目前這種配置很少,但可能以后也會(huì)增加。在這里只是闡明一個(gè)為主其他為輔的Level配置系統(tǒng)。

思考:Levels們的Actors和World有直接關(guān)系嗎? 
當(dāng)別的Level被添加進(jìn)當(dāng)前World之后,我們能直接在WorldOutliner里看到其他Level的Actor們。 
 
但這并不代表著World直接引用了Level里的Actor們。TActorIteratorBase(World的Actor迭代器)內(nèi)部的實(shí)現(xiàn)也只是在遍歷Levels來獲得所有Actor。當(dāng)然World為了更快速的操作Controllers和Pawn也都保存了引用。但Levels卻共享著World的一個(gè)PhysicsScene,這也意味著Levels里的Actors的物理實(shí)體其實(shí)都是在World里的,這也好理解,畢竟物理的碰撞之類的當(dāng)然要是全局的了。再說到導(dǎo)航,World在拼接Level的時(shí)候,也是會(huì)同時(shí)把兩個(gè)Level的導(dǎo)航網(wǎng)格給“拼接”起來的。當(dāng)然目前還不是深入細(xì)節(jié)的時(shí)候,現(xiàn)在只要從大局上明白World-Level-Actor的關(guān)系。

思考:為什么要在Level里保存Actors,而不是把所有Map的Actors配置都生成在World一個(gè)總Actors里? 
這肯定也是一種實(shí)現(xiàn)方式,好處是把整個(gè)World看成一個(gè)整體,所有的actors都從屬于world,這樣就不存在Level邊界,可以更整體的處理Actors的作用范圍和判定問題,實(shí)現(xiàn)上也少了拼接導(dǎo)航等步驟。當(dāng)然壞處也是模糊了Level邊界,這樣在加載進(jìn)一個(gè)Level之后,之后再動(dòng)態(tài)釋放,就需要再重新再從整體中抽離出部分來釋放,這個(gè)篩選過程也會(huì)產(chǎn)生比較大的損耗。試著去理解UE的權(quán)衡,應(yīng)該是盡量的把損耗平攤(這里是把Level加載釋放的損耗盡量減小),才不會(huì)產(chǎn)生比較大的幀率波動(dòng),讓玩家感覺到卡幀。

 

總結(jié)

Level作為Actor的容器,同時(shí)也劃分了World,一方面支持了Level的動(dòng)態(tài)加載,另一方面也允許了團(tuán)隊(duì)的實(shí)時(shí)協(xié)作,大家可以同時(shí)并行編輯不同的Level。一般而言,一個(gè)玩家從游戲開始到結(jié)束,UE會(huì)創(chuàng)造一個(gè)GameWorld給玩家并一直存在。玩家切換場(chǎng)景或關(guān)卡,也只是在這個(gè)World中加載釋放不同的Level。既然Level擁有了管理者(LevelScriptActor),玩家可以編寫特定關(guān)卡的邏輯,那么我們能否對(duì)World這種層次編寫邏輯呢?答案是肯定的,不過本文篇幅有限,敬請(qǐng)期待下篇。