《InsideUE4》GamePlay架構(十)總結


世界那么大,我想去看看

引言

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

游戲世界

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

眾多的各種Actor子類又組裝成了Level(GamePlay架構(二)Level和World):
AActorToULevel.png-18kB
如此每一個Level就擁有了一座Actor的森林,你可以根據自己的需要定制化Level,比如有些Level是臨時Loading場景,有些只是保存光照,有些只是一塊靜態場景。UE用Level這種細一些粒度的對象為你的想象力提供了極大的自由度,同時也能方便團隊內的平行協作。

一個個的Level,又進一步組裝成了World:
ULevelToUWorld.png-12.5kB
就像地球上的大陸板塊一樣,World允許多個Level靜態的通過位置擺放在游戲世界中,也允許運行時動態的加載關卡。

而World之間的切換,UE用了一個WorldContext來保存切換的過程信息。玩家在切換PersistentLevel的時候,實際上就相當於切換了一個World。而再往上,就是整個游戲唯一的GameInstance,由Engine對象管理着。(GamePlay架構(三)WorldContext,GameInstance,Engine)
UWorldToUEngine.png-9.8kB

到了World這一層,整個游戲的渲染對象就齊全了。但是游戲引擎並不只是渲染,因此為了讓玩家也各種方式接入World中開始游戲。GameInstance下不光保存着World,同時也存儲着Player,有着LocalPlayer用於表示本地的玩家,也有NetConnection當作遠端的連接。(GamePlay架構(八)Player):
UPlayerToUGameInstance.png-12.3kB
玩家利用Player對象接入World之后,就可以開始控制Pawn和PlayerController的生成,有了附身的對象和攝像的眼睛。最后在Engine的Tick心跳脈搏驅動下開始一幀幀的邏輯更新和渲染。

數據和邏輯

說完了游戲世界的表現組成,那么對於一個GamePlay框架而言自然需要與其配套的業務邏輯架構。GamePlay架構的后半部分就自底向上的逐一分析了各個層次的邏輯載體,按照MVC的思想,我們可以把整個游戲的GamePlay分為三大部分:表現(View)、邏輯(Controller)、數據(Model)。一圖勝千言:
StructureEasy.jpg-177.1kB
(請點擊看大圖)
最左側的是我們已經討論過的游戲世界表現部分,從最最根源的UObject和Actor,一直到UGameEngine,不斷的組合起來,形成豐富的游戲世界的各種對象。

  1. 從UObject派生下來的AActor,擁有了UObject的反射序列化網絡同步等功能,同時又通過各種Component來組裝不同組件。UE在AActor身上同時利用了繼承和組合的各自優點,同時也規避了彼此的一些缺點,我不得不說,UE在這一方面度把握得非常的平衡優雅,既不像cocos2dx那樣繼承爆炸,也不像Unity那樣走極端全部組件組合。
  2. AActor中一些需要邏輯控制的成員分化出了APawn。Pawn就像是棋盤上的棋子,或者是戰場中的兵卒。有3個基本的功能:可被Controller控制、PhysicsCollision表示和MovementInput的基本響應接口。代表了基本的邏輯控制物理表示和行走功能。根據這3個功能的定制化不同,可以派生出不同功能的的DefaultPawn、SpectatorPawn和Character。(GamePlay架構(四)Pawn)
  3. AController是用來控制APawn的一個特殊的AActor。同屬於AActor的設計,可以讓Controller享受到AActor的基本福利,而和APawn分離又可以通過組合來提供更大的靈活性,把表示和邏輯分開,獨立變化。(GamePlay架構(五)Controller)。而AController又根據用法和適用對象的不同,分化出了APlayerController來充當本地玩家的控制器,而AAIController就充當了NPC們的AI智能。(GamePlay架構(六)PlayerController和AIController)。而數據配套的就是APlayerState,可以充當AController的可網絡復制的狀態。
  4. 到了Level這一層,UE為我們提供了ALevelScriptActor(關卡藍圖)當作關卡靜態性的邏輯載體。而對於一場游戲或世界的規則,UE提供的AGameMode就只是一個虛擬的邏輯載體,可以通過PersistentLevel上的AWorldSettings上的配置創建出我們具體的AGameMode子類。AGameMode同時也是負責在具體的Level中創建出其他的Pawn和PlayerController的負責人,在Level的切換的時候AGameMode也負責協調Actor的遷移。配套的數據對象是AGameState。(GamePlay架構(七)GameMode和GameState)
  5. World構建好了,該派玩家進來了。但游戲的方式多樣,玩家的接入方式也多樣。UE為了支持各種不同的玩家模式,抽象出了UPlayer實體來實際上控制游戲中的玩家PlayerController的生成數量和方式。(GamePlay架構(八)Player)
  6. 所有的表示和邏輯匯集到一起,形成了全局唯一的UGameInstance對象,代表着整個游戲的開始和結束。同時為了方便開發者進行玩家存檔,提供了USaveGame進行全局的數據配套。(GamePlay架構(九)GameInstance)

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

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

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

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

  • 從豎直的角度來看,左側是表示,中間是邏輯,右側是數據。
    • 當我們談到表示的時候,腦袋里想的應該是一個單純的展示對象,就像一個基本的網絡物體,它可以帶一些基本的動畫,再多一些功能,也頂多只能像一個木偶,有着一些非常機械原始的行為。我們讓他前進,他可以知道左腿右腿交替着邁,但他是無知覺的。所以左側的那一串對象,你應該盡量得讓他們保持簡單。
    • 實現中間的邏輯的時候,你應該專注於邏輯本身,盡量的忘記兩旁的表示和數據。去思考哪些邏輯是表示固有的還是比較智能判斷的。哪些Controller或Mode我們應該盡量的讓它們通用,哪些就讓它們特定的負責某一塊,有些也不能強求,自己把握好度。
    • 右側的數據,同樣的保持簡單。我們把它們分離出來的目的就是為了獨立變化和在網絡間同步,注意一下別走回頭路了就好。我們應該只在此放置純數據。
  • 從水平的切面上看,依次自底向上,記住一個原則,哪個層次的應該盡量只負責哪個層次的東西,不要對上層或下層的細節知道得太多,也盡量不要逾矩越權去指手畫腳別的對象里的內務事。大家通力協作,注重隱私,保持安全距離,不就社會和諧了嘛。
    • 最底層的Component,應該只是實現一些與游戲邏輯無關的功能。理解這個“無關”是關鍵。換個游戲,你這些Component依然可以用,就是所謂的游戲無關。
    • Actor層,通過Pawn、Controller和PlayerState的合作,根據需要旗下再派生出特定的Character,或PlayerController,AIController,但它們的合作模式,三大家族的長老們已經定下了,后輩們應該盡量遵守。這一層,關鍵的地方在於分清楚哪些是操作Actor的,別向下把Actor內部的功能給抽了出來,也別大包大攬把整個游戲的玩法也管了過來。腦袋保持清醒,這一層所做的事,就是為了讓Actor們顯得更加的智能。換句話說,這些智能的Actor組合,理論上是可以在隨便哪個Level里用的。
    • Level和World層,分清楚靜態的關卡藍圖和動態可組合GameMode。靜態的意思是這個場景本身的運作機制,動態的指的是可以像切換比賽方式一樣切換一場游戲的目的。在這一層上,你得有總覽游戲大局的自覺了,咱們都是干大事的人,眼光就不要局限在那些一兵一卒那些小事了。制定好游戲規則,賦予這一場游戲以意義,是GameMode最重要的職責。注意兩點,一是腦袋里有跟弦,一旦開始聯機環境了,GameMode就升職到Server里去了,Client就沒有了,所以千萬要小心別在GameMode做些客戶端的小事;二是GameState是表示一場游戲的數據的,而PlayerState是表示Controller的數據,對象和范圍都不同,不能混了。
    • GameInstance層,一般來說Player不需要你做太多事情,UE已經幫你處理好了。雖說力量越大,責任就越大,但領導日理萬機累壞了也不行是吧。所以GameInstance作為全局的唯一邏輯對象,我們如果能不打擾他就盡量少把事推給他,否則你很快就會看着GameInstance里堆着一山東西。GameInstance身在高層,應該只盡量做一些Level之間的協調工作。而SaveGame也應該盡量只保存游戲持久的數據。

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

整體類圖

從類的繼承層次上,咱們再加深一下理解。下圖只列出了GamePlay架構里一些相關的重要的類:
StructureClassLevel.jpg-175.9kB
(請點擊看大圖)
由此也可以看出來,UE基於UObject的機制出發,構建出了紛繁復雜的游戲世界,幾乎所有的重要的類都直接或間接的繼承於UObject,都能充分利用到UObject的反射等功能,大大加強了整體框架的靈活度和表達能力。比如GamePlay中最常用到根據某個Class配置在運行時創建出特定的對象的行為就是利用了反射功能;而網絡里的屬性同步也是利用了UObject的網絡同步RPC調用;一個Level想保存成uasset文件,或者USaveGame想存檔,也都是利用了UObject的序列化;而利用了UObject的CDO(Class Default Object),在保存時候也大大節省了內存;這么多Actor對象能在編輯器里方便的編輯,也得益於UObject的屬性編輯器集成;對象互相引用的從屬關系有了UObject的垃圾回收之后我們就不用擔心會釋放問題了。想象一下如果一開始沒有設計出UObject,那么這個GamePlay框架肯定是另一番模樣了。

總結

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

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

結束語

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

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

敬請期待下個專題:UObject!

UE4.14


知乎專欄:InsideUE4

UE4深入學習QQ群: 456247757(非新手入門群,請先學習完官方文檔和視頻教程)

個人原創,未經授權,謝絕轉載!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM