上文我們說到在Actor層次,UE用Controller來充當APawn的邏輯控制者,也有了可以接受玩家輸入的PlayerController,和能自行行動的AIController。Actor的邏輯編寫介紹完了,那么本篇,我們繼續(xù)爬升,對于由Actors組成的Level這一層次,UE又是怎么控制的呢?
對Level記不太清楚的朋友,可以翻回去查看“GamePlay架構(二)Level和World”的講述,簡單概括就是World是由一個PersisitentLevel和一些subLevels組成的,PersisitentLevel切換了,相應的World也會切換。所以本文的關注點是在這么一個對象層次結構下,UE是怎么設計的,我們又能做些什么。

GameMode

Level,在游戲里的概念里,就是關卡的意思。同時作為游戲的玩家和開發(fā)者,我們總是會非常經常的提起關卡,但是關卡具體又是個什么定義呢?游戲里的哪些部分可以算是一個關卡?簡單的我們都知道有《憤怒的小鳥》或《植物大戰(zhàn)僵尸》的關卡,復雜的有大型FPS游戲里的關卡,而對于更大型的《暗黑3》或者大型無縫地圖RPG游戲《巫師3》,甚至是號稱超級廣闊宇宙《無人深空》,我們能直接了當的說出哪部分是關卡嗎?游戲行業(yè)發(fā)展如今,為了更好的組織游戲邏輯和內容資源,也發(fā)展出了一些概念來更好的理解和闡述,雖然叫法不同,不過含義理念都是相通的。比如,Cocos2dx會認為游戲就是由不同的Scene切換組成的,每個Scene又由Layer組成;Unity也認為游戲就是一個個Scene;而UE的視角的是,游戲是由一個個World組成的,World又是由Level組成的。這些概念有什么不同?
讓我們從游戲本身的機制上分析:

  • 游戲或玩家的節(jié)奏,游戲可以分成一個個階段,馬里奧里的關卡就是一個階段,而RPG游戲的一個大地圖也是一個階段。一個游戲也可能只有一個階段,比如一直在宇宙里漫游的游戲。通常一個階段結束后,會有一個結算,階段之間,玩家也能明顯感覺到切換感。
  • 游戲的機制,有時候即使是同樣的場景,玩家卻也能感覺就像在玩兩個不同的游戲,比如MOBA里的同一張地圖上的各種不同挑戰(zhàn)模式。
  • 游戲的資源劃分,有時候也能遇見同一個玩法應用在不同的場景上,比如賽車游戲的不同跑道。有時候也會在游戲的大地圖里從酷熱的沙漠到寒冷的極地。游戲開發(fā)中也總是傾向于給游戲用到的資源劃分成組的進行載入和釋放。

通過以上的分析,也和以前的一貫思路一樣,我們發(fā)現在思考“關卡”這件事情上,也是要保持頭腦清晰的分清“表示”和“邏輯”。玩法就是“邏輯”,場景就是“表示”。所以我們如果以邏輯來劃分游戲,得到的就是一個個World的概念;如果以表示來劃分,得到就是一個個Level。一場游戲中,玩法再復雜但也只有一個,場景卻可以無限大,所以可以有很多個表示拼接組裝,因此是World包含Level,而不是反過來?,F在回過頭來回想一下Cocos2dx和Unity的世界觀,它們的概念還只是在表示層,在游戲實例和關卡之間少了一個更高級的邏輯概念。
因此UE的世界觀是,World更多是邏輯的概念,而Level是資源場景表示。以《巫師3》為例,有好幾個國家之間通過傳送切換,國家內大地圖無縫漫游,顯然我們知道不可能把一個國家的所有資源都加載進內存,因此在UE里,一個國家就是許多個Level拼接的,而一個國家就是一個World,它們可以有不同的模式玩法。但畢竟AAA游戲很少,通常的,我們的游戲比較簡單的用一個Level就夠了,否則這個場景表示的概念就應該叫Area更合適了,也因此通常的這里的Level也常常對應游戲里玩家面對的"關卡",也因此UE里Level的Settings叫做WorldSettings了。
厘清了這些概念了之后,我們就知道,當我們在談Level的業(yè)務邏輯控制的時候,我們實際上談的是World的業(yè)務邏輯。按照UE的設計理念和經過Controller的經歷,我想我也不用多解釋了從Actor再派生出一個WorldController的方式了,可以直接的享受Actor已經提供的一切福利。一個World的Controller想不出有什么需要展示渲染的,因此可以直接從AInfo派生吧。哦,WorldController是我瞎編的,在UE3里它叫做GameInfo,到了UE4它改名為了GameMode?;\統(tǒng)的講,一個World就是一個Game,把玩法叫做Mode,我們應該也能接受吧。那我們來看看它:

既然勇敢的承擔了游戲邏輯的職責,說他是AInfo家族里的扛把子也不為過,因此GameMode身為一場游戲的唯一邏輯操縱者身兼重任,在功能實現上有許多的接口,但主要可以分為以下幾大塊:

  1. Class登記,GameMode里登記了游戲里基本需要的類型信息,在需要的時候通過UClass的反射可以自動Spawn出相應的對象來添加進關卡中。前文說過的Controller的類型登記也是在此,GameMode就是比Controller更高一級的領導。
  2. 游戲內實體的Spawn,不光登記,GameMode既然作為一場游戲的主要負責人,那么游戲的加載釋放過程中涉及到的實體的產生,包括玩家Pawn和PlayerController,AIController也都是由GameMode負責。最主要的SpawnDefaultPawnFor、SpawnPlayerController、ShouldSpawnAtStartSpot這一系列函數都是在接管玩家實體的生成和釋放,玩家進入該游戲的過程叫做Login(和服務器統(tǒng)一),也控制進來后在什么位置,等等這些實體管理的工作。GameMode也控制著本場游戲支持的玩家、旁觀者和AI實體的數目。
  3. 游戲的進度,一個游戲支不支持暫停,怎么重啟等這些涉及到游戲內狀態(tài)的操作也都是GameMode的工作之一,SetPause、ResartPlayer等函數可以控制相應邏輯。
  4. Level的切換,或者說World的切換更加合適,GameMode也決定了剛進入一場游戲的時候是否應該開始播放開場動畫(cinematic),也決定了當要切換到下一個關卡時是否要bUseSeamlessTravel,一旦開啟后,你可以重載GameMode和PlayerController的GetSeamlessTravelActorList方法和GetSeamlessTravelActorList來指定哪些Actors不被釋放而進入下一個World的Level。
  5. 多人游戲的步調同步,在多人游戲的時候,我們常常需要等所有加入的玩家連上之后,載入地圖完畢后才能一起開始邏輯。因此UE提供了一個MatchState來指定一場游戲運行的狀態(tài),意義看名稱也是不言自明的,就是用了一個狀態(tài)機來標記開始和結束的狀態(tài),并觸發(fā)各種回調。

    /** Possible state of the current match, where a match is all the gameplay that happens on a single map */ namespace MatchState
    { extern ENGINE_API const FName EnteringMap; // We are entering this map, actors are not yet ticking extern ENGINE_API const FName WaitingToStart; // Actors are ticking, but the match has not yet started extern ENGINE_API const FName InProgress; // Normal gameplay is occurring. Specific games will have their own state machine inside this state extern ENGINE_API const FName WaitingPostMatch; // Match has ended so we aren't accepting new players, but actors are still ticking extern ENGINE_API const FName LeavingMap; // We are transitioning out of the map to another location extern ENGINE_API const FName Aborted; // Match has failed due to network issues or other problems, cannot continue }

思考:多個Level配置不同的GameMode時采用的是哪一個GameMode?
我們知道除了配置全局的GameModeClass之外,我們還能為每個Level單獨的配置不同的GameModeClass。但是當一個World由多個Level組成的時候,這樣就相當于配置了多個GameModeClass,那么應用的是哪一個?首先第一個原則需要記住的就是,一個World里只會有一個GameMode實例,否則肯定亂套了。因此當有多個Level的時候,一定是PersisitentLevel和多個StreamingLevel,這時就算它們配置了不同的GameModeClass,UE也只會為第一次創(chuàng)建World時加載PersisitentLevel的時候創(chuàng)建GameMode,在后續(xù)的LoadStreamingLevel時候,并不會再動態(tài)創(chuàng)建出別的GameMode,所以GameMode從始至終只有一個,PersisitentLevel的那個。

思考:Level遷移時GameMode是否保持一致?
在在travelling的時候,如果下一個Level的配置的GameModeClass和當前的不同,那么遷移后是哪個GameMode?
無論travelling采用哪種方式,當前的World都會被釋放掉,然后加載創(chuàng)建新的World。但這個過程中,有點區(qū)別的是根據bUseSeamlessTravel的不同,UE可以選擇哪些Actor遷移到下一個World中去(實現方式是先創(chuàng)建個中間過渡World進行二段遷移(為了避免同時加載進兩個大地圖撐爆內存),具體見引用3)。分兩種情況:
不開啟bUseSeamlessTravel,那么在travelling的時候(ServerTravel或ClientTravel),當前的World會被釋放,所以當前的GameMode就被釋放掉。新的World加載,就會根據新的GameModeClass創(chuàng)建新的GameMode。所以這時是不同的。
開啟bUseSeamlessTravel,travelling時,當前World的GameMode會調用GetSeamlessTravelActorList:

void AGameMode::GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList)
{
    UWorld* World = GetWorld(); // Get allocations for the elements we're going to add handled in one go const int32 ActorsToAddCount = World->GameState->PlayerArray.Num() + (bToTransition ? 3 : 0);
    ActorList.Reserve(ActorsToAddCount); // always keep PlayerStates, so that after we restart we can keep players on the same team, etc ActorList.Append(World->GameState->PlayerArray); if (bToTransition)
    { // keep ourselves until we transition to the final destination ActorList.Add(this); // keep general game state until we transition to the final destination ActorList.Add(World->GameState); // keep the game session state until we transition to the final destination ActorList.Add(GameSession); // If adding in this section best to increase the literal above for the ActorsToAddCount }
}

在第一步從CurrentWorld到TransitionWorld的遷移時候,bToTransition==true,這個時候GameMode也會遷移進TransitionWorld(TransitionMap可以在ProjectSettings里配置),也包括GameState和GameSession,然后CurrentWorld釋放掉。第二步從TransitionWorld到NewWorld的遷移,GameMode(已經在TransitionWorld中了)會再次調用GetSeamlessTravelActorList,這個時候bToTransition==false,所以第二次的時候如代碼所見當前的GameMode、GameState和GameSession就被排除在外了。這樣NewWorld再繼續(xù)InitWorld的時候,一發(fā)現當前沒有GameMode,就會根據配置的GameModeClass重新生成一個出來。所以這個時候GameMode也是不同的。
結論是,UE的流程travelling,GameMode在新的World里是會新生成一個的,即使Class類型一致,即使bUseSeamlessTravel,因此在travelling的時候要小心GameMode里保存的狀態(tài)丟失。不過Pawn和Controller默認是一致的。

思考:哪些邏輯應該寫在GameMode里?哪些應該寫在Level Blueprint里?
我們依舊要問這個老土的問題。根據我們前面的知識,我們知道每個Level其實也是有自己的LevelScriptActor的,那么這兩個有什么區(qū)別?可以從這幾個方面來回答:

  • 概念上,Level是表示,World是邏輯,一個World如果有很多個Level拼在一起,那么也就是有了很多個LevelScriptActor,無法想象在那么多個地方寫一個完整的游戲邏輯。所以GameMode應該專注于邏輯的實現,而LevelScriptActor應該專注于本Level的表示邏輯,比如改變Level內某些Actor的運動軌跡,或者某一個區(qū)域的重力,或者觸發(fā)一段特效或動畫。而GameMode應該專注于玩法,比如勝利條件,怪物刷新等。
  • 組合上,同Controller應用到Pawn一樣道理,因為GameMode是可以應用在不同的Level的,所以通用的玩法應該放在GameMode里。
  • GameMode只在Server存在(單機游戲也是Server),對于已經連接上Server的Client來說,因為游戲的狀態(tài)都是由Sever決定的,Client只是負責展示,所以Client上是沒有GameMode的,但是有LevelScriptActor,所以GameMode里不要寫Client特定相關的邏輯,比如操作UI等。但是LevelScriptActor還是有的,而且支持RPC,即使如此,LevelScriptActor還是應該只專注于表現,比如網絡中觸發(fā)一個特效火焰。至于UI,可以通過PlayerController的RPC,然后轉發(fā)到GameInstance來操作。
  • 跟下層的PlayerController比較,GameMode關心的是構建一個游戲本身的玩法,PlayerController關心的玩家的行為。這兩個行為是獨立正交可以自由組合的。所以想想哪些邏輯屬于游戲,哪些屬于玩家,就應該清楚寫在哪里了。
  • 跟上層的GameInstance比較,GameInstance關注的是更高層的不同World之間的邏輯,雖然有時候他也把手伸下來做些UI的管理工作,不過嚴謹來說,在UE里UI是獨立于World的一個結構,所以也還算能理解。因此可以把不同GameMode之間協調的工作交給GameInstance,而GameMode只專注自己的玩法世界。

GameState

上回說到了APlayerState用來保存玩家的游戲數據,那么同樣的,對于一場游戲,也需要一個State來保存當前游戲的狀態(tài)數據,比如任務數據等。跟APlayerState一樣,GameState也選擇從AInfo里繼承,這樣在網絡環(huán)境里也可以Replicated到多個Client上面去。

比較簡單,第一個MatchState和相關的回調就是為了在網絡中傳播同步游戲的狀態(tài)使用的(記得GameMode在Client并不存在,但是GameState是存在的,所以可以通過它來復制),第二部分是玩家狀態(tài)列表,同樣的如果在Client1想看到Client2的游戲狀態(tài)數據,則Client2的PlayerState就必須廣播過來,因此GameState把當前Server的PlayerState都收集了過來,方便訪問使用。
關于使用,開發(fā)者可以自定義GameState子類來存儲本GameMode的運行過程中產生的數據(那些想要replicated的!),如果是GameMode游戲運行的一些數據,又不想要所有的客戶端都可以看到,則也可以寫在GameMode的成員變量中。重復遍,PlayerState是玩家自己的游戲數據,GameInstance里是程序運行的全局數據。

GameSession

是在網絡聯機游戲中針對Session使用的一個方便的管理類,并不存儲數據,本文重點也不在網絡,故不做過多解釋,可暫時忽略,留待網絡章節(jié)再討論。在單機游戲中,也存在該類對象用來LoginPlayer,不過因為只是作為輔助類,那也可看作GameMode本身的功能,所以不做過多討論。

總結

現在,我們也算討論完了Level(World)層次的控制,對于一場游戲而言,我們最關心的是怎么協調好整個場景的表現(LevelBlueprint)和游戲玩法的編寫(GameMode)。UE再次用Actor分化派生的思想,用同樣套路的AGameMode和AGameState支持了玩法和表現的解耦分離和自由組合,并很好的支持了網絡間狀態(tài)的同步。同時也提供了一個邏輯的實體來負責創(chuàng)建關系內那些關鍵的Pawn和Controller們,在關卡切換(World)的時候,也有了一個負責對象來處理一些本游戲的特定情況處理。

我們的邏輯之旅還沒到終點,讓我們繼續(xù)爬升,下篇將介紹Player。

修訂

在筆者書寫本篇的同時(UE4.13.2),UE同時也完成了4.14的preview3的工作,roadmap里“GameMode Cleanup”的工作也已經完成了,第二天發(fā)現4.14正式發(fā)布了。因此為了緊跟UE最新潮流時尚,以后要是文章內容所涉及內容被UE修改完善優(yōu)化的,也會采用修訂的方式進行補充說明,之后不再特意作此聲明。

4.14 GameMode,GameState的清理

根據搜索到的最早記錄"[Request/Improvment] GameMode cleanup."(09-14-2014),是有人抱怨當前的GameMode實現了太多的默認邏輯(例如多人的Match),雖然方便了一些人使用,但是也確實加大了理解的難度,并且有時候還得去屏蔽刪除一些默認邏輯。然后順便吐槽了一番AActor里的Damage,筆者也表示這確實不是AActor應該管的事情。
言歸正傳,UE在2016-08-24的時候開始加進roadmap,并終于在4.14里實現完成了。如前所述,就是把GameMode和GameState的一些共同最基礎部分抽到基類AGameModeBase和AGameStateBase里,并把現在的GameMode和GameState依然當作多人聯機的默認實現。所以以后大家如果想實現一個比較簡單的單機GameMode就可以直接從AGameModeBase里繼承了。

可以看到,其實就是把MatchState給往下拉了一層,并把一些多玩家控制的邏輯,合起來就是網絡聯機游戲的默認邏輯給抽離開了。同樣的對于GameState也做了處理:

把MatchState也抽離到了下層,并增加了幾個方便的字段引用(如AuthorityGameMode)??傮w功能職責架構上還是沒有什么大變化的,嚇死我了。