世界那么大,我想去看看

引言

通過對前九篇的介紹,至此我們已經(jīng)了解了UE里的游戲世界組織方式和游戲業(yè)務(wù)邏輯的控制。行百里者半九十,前述的篇章里我們的目光往往專注在于特定一個類或者對象,一方面固然可以讓內(nèi)容更有針對性,但另一方面也有了身在山中不見山的困惑。本文作為GamePlay章節(jié)的最終章,就是要回顧我們之前探討過的內(nèi)容,以一個更高層總覽的眼光,把之前的所有內(nèi)容有機(jī)組織起來,思考整體的結(jié)構(gòu)和數(shù)據(jù)及邏輯的流向。

游戲世界

如果我們在最初篇所問的,如果讓你來制作一款3D游戲引擎,你會怎么設(shè)計其結(jié)構(gòu)?已經(jīng)知道,在UE的眼里,游戲世界的萬物皆Actor,Actor再通過Component組裝功能。Actor又通過UChildActorComponent實現(xiàn)Actor之間的父子嵌套。(GamePlay架構(gòu)(一)Actor和Component)

眾多的各種Actor子類又組裝成了Level(GamePlay架構(gòu)(二)Level和World):

如此每一個Level就擁有了一座Actor的森林,你可以根據(jù)自己的需要定制化Level,比如有些Level是臨時Loading場景,有些只是保存光照,有些只是一塊靜態(tài)場景。UE用Level這種細(xì)一些粒度的對象為你的想象力提供了極大的自由度,同時也能方便團(tuán)隊內(nèi)的平行協(xié)作。

一個個的Level,又進(jìn)一步組裝成了World:

就像地球上的大陸板塊一樣,World允許多個Level靜態(tài)的通過位置擺放在游戲世界中,也允許運(yùn)行時動態(tài)的加載關(guān)卡。

而World之間的切換,UE用了一個WorldContext來保存切換的過程信息。玩家在切換PersistentLevel的時候,實際上就相當(dāng)于切換了一個World。而再往上,就是整個游戲唯一的GameInstance,由Engine對象管理著。(GamePlay架構(gòu)(三)WorldContext,GameInstance,Engine)

到了World這一層,整個游戲的渲染對象就齊全了。但是游戲引擎并不只是渲染,因此為了讓玩家也各種方式接入World中開始游戲。GameInstance下不光保存著World,同時也存儲著Player,有著LocalPlayer用于表示本地的玩家,也有NetConnection當(dāng)作遠(yuǎn)端的連接。(GamePlay架構(gòu)(八)Player):

玩家利用Player對象接入World之后,就可以開始控制Pawn和PlayerController的生成,有了附身的對象和攝像的眼睛。最后在Engine的Tick心跳脈搏驅(qū)動下開始一幀幀的邏輯更新和渲染。

數(shù)據(jù)和邏輯

說完了游戲世界的表現(xiàn)組成,那么對于一個GamePlay框架而言自然需要與其配套的業(yè)務(wù)邏輯架構(gòu)。GamePlay架構(gòu)的后半部分就自底向上的逐一分析了各個層次的邏輯載體,按照MVC的思想,我們可以把整個游戲的GamePlay分為三大部分:表現(xiàn)(View)、邏輯(Controller)、數(shù)據(jù)(Model)。一圖勝千言:

(請點擊看大圖)
最左側(cè)的是我們已經(jīng)討論過的游戲世界表現(xiàn)部分,從最最根源的UObject和Actor,一直到UGameEngine,不斷的組合起來,形成豐富的游戲世界的各種對象。

  1. 從UObject派生下來的AActor,擁有了UObject的反射序列化網(wǎng)絡(luò)同步等功能,同時又通過各種Component來組裝不同組件。UE在AActor身上同時利用了繼承和組合的各自優(yōu)點,同時也規(guī)避了彼此的一些缺點,我不得不說,UE在這一方面度把握得非常的平衡優(yōu)雅,既不像cocos2dx那樣繼承爆炸,也不像Unity那樣走極端全部組件組合。

  2. AActor中一些需要邏輯控制的成員分化出了APawn。Pawn就像是棋盤上的棋子,或者是戰(zhàn)場中的兵卒。有3個基本的功能:可被Controller控制、PhysicsCollision表示和MovementInput的基本響應(yīng)接口。代表了基本的邏輯控制物理表示和行走功能。根據(jù)這3個功能的定制化不同,可以派生出不同功能的的DefaultPawn、SpectatorPawn和Character。(GamePlay架構(gòu)(四)Pawn)

  3. AController是用來控制APawn的一個特殊的AActor。同屬于AActor的設(shè)計,可以讓Controller享受到AActor的基本福利,而和APawn分離又可以通過組合來提供更大的靈活性,把表示和邏輯分開,獨立變化。(GamePlay架構(gòu)(五)Controller)。而AController又根據(jù)用法和適用對象的不同,分化出了APlayerController來充當(dāng)本地玩家的控制器,而AAIController就充當(dāng)了NPC們的AI智能。(GamePlay架構(gòu)(六)PlayerController和AIController)。而數(shù)據(jù)配套的就是APlayerState,可以充當(dāng)AController的可網(wǎng)絡(luò)復(fù)制的狀態(tài)。

  4. 到了Level這一層,UE為我們提供了ALevelScriptActor(關(guān)卡藍(lán)圖)當(dāng)作關(guān)卡靜態(tài)性的邏輯載體。而對于一場游戲或世界的規(guī)則,UE提供的AGameMode就只是一個虛擬的邏輯載體,可以通過PersistentLevel上的AWorldSettings上的配置創(chuàng)建出我們具體的AGameMode子類。AGameMode同時也是負(fù)責(zé)在具體的Level中創(chuàng)建出其他的Pawn和PlayerController的負(fù)責(zé)人,在Level的切換的時候AGameMode也負(fù)責(zé)協(xié)調(diào)Actor的遷移。配套的數(shù)據(jù)對象是AGameState。(GamePlay架構(gòu)(七)GameMode和GameState)

  5. World構(gòu)建好了,該派玩家進(jìn)來了。但游戲的方式多樣,玩家的接入方式也多樣。UE為了支持各種不同的玩家模式,抽象出了UPlayer實體來實際上控制游戲中的玩家PlayerController的生成數(shù)量和方式。(GamePlay架構(gòu)(八)Player)

  6. 所有的表示和邏輯匯集到一起,形成了全局唯一的UGameInstance對象,代表著整個游戲的開始和結(jié)束。同時為了方便開發(fā)者進(jìn)行玩家存檔,提供了USaveGame進(jìn)行全局的數(shù)據(jù)配套。(GamePlay架構(gòu)(九)GameInstance)

UE為我們提供了這些GamePlay的對象,說多其實也不多,而且其實也是這么優(yōu)雅有機(jī)的結(jié)合在一起。但是仍然會把一些朋友給迷惑住了,常常就會問哪些邏輯該寫在哪里,哪些數(shù)據(jù)該放在哪里,這么多個對象,好像哪個都可以。比如Pawn,有些人就會說我就是直接在Pawn里寫邏輯和數(shù)據(jù),游戲也運(yùn)行的好好的,也沒什么不對。

如果你是一個已經(jīng)對設(shè)計架構(gòu)了然于心,也預(yù)見到了游戲未來發(fā)展變化,那么這么直接干也確實比較快速方便。但是這么做其實隱含了兩個前提,一是這個Pawn的邏輯足夠簡單,把MVC的三者混合在一起依然不超過你的心智負(fù)擔(dān);二是已經(jīng)斷絕了邏輯和數(shù)據(jù)的分離,如果以后本地想復(fù)用一些邏輯創(chuàng)建另一個Pawn就會很麻煩,而且未來聯(lián)機(jī)多玩家的狀態(tài)復(fù)制也不支持。但說回來,人類的一個最常見的問題就是自大,對自己能力的過度自信,對未來變化的虛假掌控感。程序員在自己的編程世界里,呼風(fēng)喚雨操作內(nèi)存設(shè)備慣了,這種強(qiáng)大的掌控感非常容易地就外延到其他方面去了。你現(xiàn)在寫的代碼,過幾個月后再回頭看,是不是經(jīng)常覺得非常糟糕?那奇怪了,當(dāng)初寫的時候怎么就感覺信心滿滿呢?所以踩坑多了的人就會自然的保守一些。另一方面,作為團(tuán)隊里的技術(shù)高手或老人,我個人覺得也有支持同行和提攜后輩的責(zé)任,對自己而言只是多花一點點力氣,卻為別人樹立一個清晰的程序結(jié)構(gòu)典范,也傳播了設(shè)計思想。程序員何苦為難程序員。

但還有一些人喜歡那么硬懟著干的原因要嘛是對未來的可預(yù)見性不足(經(jīng)驗不足),要嘛是對程序設(shè)計的基本原則不夠了解(程序能力不夠),比如最簡單的“單一職責(zé)”。在新手期,面對著UE的程序世界,雖然在已經(jīng)懂的人眼里就那么幾個對象,但是在新手眼里,往往就感覺復(fù)雜無比,面對未知,我們本能的反應(yīng)是逃避,往往就傾向于哪些看起來這么用能工作,就像玩游戲一樣,形成了你的“專屬套路”。跟窮人忙于工作而沒力氣提高自己是一個道理。相信我,所有的高手都是從小白過來的,我敢保證,他出生的時候腦袋也肯定是一片空白!區(qū)別是有些人后來不怕麻煩的勤能補(bǔ)拙,他努力的去理解這種設(shè)計模式的優(yōu)劣,不局限于自己已經(jīng)掌握的一片舒適區(qū)內(nèi),努力去設(shè)想未來的各種變化和應(yīng)對之法,最終形成自己的獨立思考。高手只是比新手懂得更多想得更多一些而已。

閑話說完。在分析UE這么一個GamePlay系統(tǒng)的時候,就像UML有各種圖一樣,我們也應(yīng)該從各個切面去分析它的構(gòu)成。這里有兩大基本原則:單一職責(zé)和變化隔離,但也可以說只有一個。所有的程序設(shè)計模式都只是在抽象變化,把變化都抽離開了,剩下的不就是單一職責(zé)了嘛。所以UE里對MVC的實踐其實也只是在不斷抽離出各個對象的變化部分,把Pawn的邏輯抽出來是Controller,把數(shù)據(jù)抽出來是PlayerState。把World的Level靜態(tài)邏輯抽出來是關(guān)卡藍(lán)圖,把動態(tài)的游戲玩法抽離出來是GameMode,把游戲數(shù)據(jù)抽離出來是GameState。具體的每個層次的數(shù)據(jù)和邏輯的關(guān)系前文已經(jīng)一一詳細(xì)說過了,此處就不再贅述了。但也再次著重探討一些分析方法:

  • 從豎直的角度來看,左側(cè)是表示,中間是邏輯,右側(cè)是數(shù)據(jù)。

    • 當(dāng)我們談到表示的時候,腦袋里想的應(yīng)該是一個單純的展示對象,就像一個基本的網(wǎng)絡(luò)物體,它可以帶一些基本的動畫,再多一些功能,也頂多只能像一個木偶,有著一些非常機(jī)械原始的行為。我們讓他前進(jìn),他可以知道左腿右腿交替著邁,但他是無知覺的。所以左側(cè)的那一串對象,你應(yīng)該盡量得讓他們保持簡單。

    • 實現(xiàn)中間的邏輯的時候,你應(yīng)該專注于邏輯本身,盡量的忘記兩旁的表示和數(shù)據(jù)。去思考哪些邏輯是表示固有的還是比較智能判斷的。哪些Controller或Mode我們應(yīng)該盡量的讓它們通用,哪些就讓它們特定的負(fù)責(zé)某一塊,有些也不能強(qiáng)求,自己把握好度。

    • 右側(cè)的數(shù)據(jù),同樣的保持簡單。我們把它們分離出來的目的就是為了獨立變化和在網(wǎng)絡(luò)間同步,注意一下別走回頭路了就好。我們應(yīng)該只在此放置純數(shù)據(jù)。

  • 從水平的切面上看,依次自底向上,記住一個原則,哪個層次的應(yīng)該盡量只負(fù)責(zé)哪個層次的東西,不要對上層或下層的細(xì)節(jié)知道得太多,也盡量不要逾矩越權(quán)去指手畫腳別的對象里的內(nèi)務(wù)事。大家通力協(xié)作,注重隱私,保持安全距離,不就社會和諧了嘛。

    • 最底層的Component,應(yīng)該只是實現(xiàn)一些與游戲邏輯無關(guān)的功能。理解這個“無關(guān)”是關(guān)鍵。換個游戲,你這些Component依然可以用,就是所謂的游戲無關(guān)。

    • Actor層,通過Pawn、Controller和PlayerState的合作,根據(jù)需要旗下再派生出特定的Character,或PlayerController,AIController,但它們的合作模式,三大家族的長老們已經(jīng)定下了,后輩們應(yīng)該盡量遵守。這一層,關(guān)鍵的地方在于分清楚哪些是操作Actor的,別向下把Actor內(nèi)部的功能給抽了出來,也別大包大攬把整個游戲的玩法也管了過來。腦袋保持清醒,這一層所做的事,就是為了讓Actor們顯得更加的智能。換句話說,這些智能的Actor組合,理論上是可以在隨便哪個Level里用的。

    • Level和World層,分清楚靜態(tài)的關(guān)卡藍(lán)圖和動態(tài)可組合GameMode。靜態(tài)的意思是這個場景本身的運(yùn)作機(jī)制,動態(tài)的指的是可以像切換比賽方式一樣切換一場游戲的目的。在這一層上,你得有總覽游戲大局的自覺了,咱們都是干大事的人,眼光就不要局限在那些一兵一卒那些小事了。制定好游戲規(guī)則,賦予這一場游戲以意義,是GameMode最重要的職責(zé)。注意兩點,一是腦袋里有跟弦,一旦開始聯(lián)機(jī)環(huán)境了,GameMode就升職到Server里去了,Client就沒有了,所以千萬要小心別在GameMode做些客戶端的小事;二是GameState是表示一場游戲的數(shù)據(jù)的,而PlayerState是表示Controller的數(shù)據(jù),對象和范圍都不同,不能混了。

    • GameInstance層,一般來說Player不需要你做太多事情,UE已經(jīng)幫你處理好了。雖說力量越大,責(zé)任就越大,但領(lǐng)導(dǎo)日理萬機(jī)累壞了也不行是吧。所以GameInstance作為全局的唯一邏輯對象,我們?nèi)绻懿淮驍_他就盡量少把事推給他,否則你很快就會看著GameInstance里堆著一山東西。GameInstance身在高層,應(yīng)該只盡量做一些Level之間的協(xié)調(diào)工作。而SaveGame也應(yīng)該盡量只保存游戲持久的數(shù)據(jù)。

自始至終,回顧一下每個類的本身的職責(zé),該是他的就是他的,別人的不要搶。讀者朋友們,如果到此覺得似乎懂了一些,但還是覺得不夠深刻理解的話,也沒關(guān)系,凡事不能一蹴而就,在開發(fā)過程中多想多琢磨自然而然就會慢慢領(lǐng)悟了。

整體類圖

從類的繼承層次上,咱們再加深一下理解。下圖只列出了GamePlay架構(gòu)里一些相關(guān)的重要的類:

(請點擊看大圖)
由此也可以看出來,UE基于UObject的機(jī)制出發(fā),構(gòu)建出了紛繁復(fù)雜的游戲世界,幾乎所有的重要的類都直接或間接的繼承于UObject,都能充分利用到UObject的反射等功能,大大加強(qiáng)了整體框架的靈活度和表達(dá)能力。比如GamePlay中最常用到根據(jù)某個Class配置在運(yùn)行時創(chuàng)建出特定的對象的行為就是利用了反射功能;而網(wǎng)絡(luò)里的屬性同步也是利用了UObject的網(wǎng)絡(luò)同步RPC調(diào)用;一個Level想保存成uasset文件,或者USaveGame想存檔,也都是利用了UObject的序列化;而利用了UObject的CDO(Class Default Object),在保存時候也大大節(jié)省了內(nèi)存;這么多Actor對象能在編輯器里方便的編輯,也得益于UObject的屬性編輯器集成;對象互相引用的從屬關(guān)系有了UObject的垃圾回收之后我們就不用擔(dān)心會釋放問題了。想象一下如果一開始沒有設(shè)計出UObject,那么這個GamePlay框架肯定是另一番模樣了。

總結(jié)

對于GamePlay我們從構(gòu)建游戲世界開始,再到一層層的邏輯控制,本篇也從各個切面上總結(jié)歸納了整體架構(gòu)。希望讀者們好好領(lǐng)會UE的GamePlay架構(gòu)思想,別貪快,整體上慢慢琢磨以上的架構(gòu)圖,細(xì)節(jié)上可以回顧過往的單篇來細(xì)了解。

對于這一套UE提供的GamePlay框架,我們既然選擇了用UE引擎,那么自然就應(yīng)該想著怎么充分利用好它。框架就是你如果在它的規(guī)則下辦事,那它就是事半功倍的助力器,你會常常發(fā)現(xiàn)UE怎么連這個也幫你做完了;而如果你在不了解的情況下想逆著它行事,就常常感受到怎么哪里都受到束縛。我們對于框架的理念應(yīng)該就像是對待一輛汽車一般,我們關(guān)心的是怎么駕駛它到達(dá)想要的目的他,而不是折騰著怪它四個輪子不能按照你的心意朝不同方向亂轉(zhuǎn)。對比隔壁的Cocos2dx、或Unity、或CryEngine,UE能夠提供這么一個完善的GamePlay框架,對我們開發(fā)者而言,是一件幸福的事,不是嗎?

結(jié)束語

完結(jié)撒花!GamePlay大章節(jié)也終于結(jié)束了,最開始是本著怎么盡早盡大的能幫助到讀者朋友們,所以選擇了GamePlay作為起始章節(jié)。相信GamePlay也是開發(fā)者們?nèi)粘i_發(fā)過程中接觸最多,也是有可能混淆最多,概念不清,很容易用錯的一塊主題。在介紹GamePlay的時候,更多的重點是在于介紹各對象的職責(zé)和關(guān)聯(lián),所以更多是用類圖來描述結(jié)構(gòu),反而對源碼進(jìn)行剖析的機(jī)會不多,但讀者們可以自己去閱讀驗證。希望GamePlay架構(gòu)的一系列十篇文章能切實地幫助到你們。

而下個專題,根據(jù)QQ群友們的投票反饋,決定了是UObject!有相當(dāng)部分開發(fā)人員,可能不知道也不太關(guān)心UObject的內(nèi)部機(jī)制。清楚了UObject,確實對于開發(fā)游戲并沒有多少直接的提升,但《InsideUE4》系列教程的初衷就是為了深入到引擎內(nèi)部提高開發(fā)者人員的內(nèi)功。對于有志于想掌握好UE的開發(fā)者而言,分析一個游戲引擎,如果只是一直停留在高層的交互,而對于最底層的對象系統(tǒng)不了解的話,那就像云端行走一般,自身感覺飄飄然,但是總免不了內(nèi)心里有些不安,學(xué)習(xí)和使用的腳步也會顯得虛浮。因此在下個專題,我們將插入UObject的最最深處,把UObject扒得一毛不掛,慢慢領(lǐng)會她的美妙!我們終于有機(jī)會得償心愿,細(xì)細(xì)把玩一句句源碼,了解關(guān)于UObject的RTTI、反射、GC、序列化等等的內(nèi)容。如果你也曾經(jīng)好奇NewObject里發(fā)生了些什么、困惑CreateSubObject為何只能在構(gòu)造函數(shù)里調(diào)用、不解GC是如何把對象給釋放掉了、uasset文件里是些什么……

敬請期待下個專題:UObject!