《InsideUE4》-6-GamePlay架構(五)Controller

《InsideUE4》-6-GamePlay架構(五)Controller

Tags: InsideUE4 GamePlay

那一天
Pawn又回想起了
被Controller所支配的恐懼html

引言

如上文所述,UE從Actor中分化了一些專門可供玩家「控制」的Pawn,那咱們這篇就專門來談談該怎麼個控制法!
所謂的控制,本質指的就是咱們遊戲的業務邏輯。好比說玩家按A鍵,角色自動找一個最近的敵人並攻擊,這個自動尋找目標並攻擊的邏輯過程,就是咱們所談的控制。
Note1:重申一下,Controller特別是PlayerController,跟網絡,AI和Input的關係都很是的緊密,目前都暫且不討論,留待各自模塊章節再敘述。程序員

MVC

無論是遊戲,仍是其餘App,Web或Server等,本質上都是程序,因此也都是或多或少須要一些程序邏輯。從1843年拜倫的女兒Ada Lovelace用穿孔機編寫第一個程序開始,到2016的今天咱們能方便地用藍圖連線組織程序邏輯,應該歸功於一代代軟件工程師們孜孜不倦的探索。時代在發展,技術在進步,軟件也愈趨於複雜多變,不少軟件的龐大也已經超越了我的的理解容量極限(UE?),所以咱們就愈來愈須要設計方法來讓咱們可管理龐大的複雜度。幾十年的迭代,舊的模型被放棄,新的模型被提出驗證,工程師們在這過程當中總結積累出了一些設計模式。最負有盛名的應該是GOF的《設計模式》,以及MVC,MVP,MVVM等。本文的重點不在於細談論各類設計模式,若是有對設計模式不清楚的讀者,請務必仔細去研究學習,因UE如此龐大的代碼框架也是充斥着各類設計模式的應用,設計模式理解得越好,也越能理解UE的框架設計。
言歸正傳,設計模式的本質就是抽象變化。若是依照純樸的"程序=數據+算法"的結構來看,再算上用於用戶顯示和輸入的界面,那麼就獲得「程序=數據+算法+顯示」。這三大基本塊(數據,算法,顯示)構成了程序的三大變化,而如何把這三者「+」到一塊兒,用的就是咱們的種種設計框架模式。
典型的,對於遊戲:算法

  • 「顯示」指的是遊戲的UI,是屏幕上顯示的3D畫面,或是手柄上的輸入和震動,也能夠是VR頭盔的鏡片和定位,是與玩家直接交互的載體;
  • 「數據」指的是Mesh,Material,Actor,Level等各類元素組織起來的內存數據表示;
  • 「算法」能夠是各類渲染算法,物理模擬,AI尋路,本文我們就先暫時特指遊戲開發者們編寫的遊戲業務邏輯。

抽象這三個變化,並概括關係,就是典型的MVC模式了:
MVC.png-30.3kB
有些人可能會說MVC是UI專用的模式,如IOS的MVC或WPF的MVVM,也或者說由於遊戲的類型千差萬別因此一個通用的框架並不能都適用,所以就有一點點想要「返璞歸真」的意味,以爲遊戲引擎只須要提供一個基本的渲染框架,其餘的邏輯框架不須要設計複雜,開發者們可自行根據遊戲類型再設計。這種觀點有必定的道理,對於簡單的遊戲或Demo,確實也還不到須要「設計」的地步;而對於複雜大型的遊戲,須要的架構知識也確實遠不是MVC這麼簡單。但缺點在於,說這話的人要嘛就已是架構高手,各類設計模式信手拈來,早已經到了無招勝有招的地步;要嘛就是迴避了問題,遊戲也是軟件,軟件的固有複雜度擺在那裏,總得須要個辦法去解決,今天若是咱們不是在探討嘗試用MVC模式去掌控它,也是在談一個別的名字的模式。在我看來,一個好的遊戲引擎,應該是能盡力的幫助用戶,並減小麻煩。MV固然也有它的缺陷和不足,因此咱們應該研究的是UE爲什麼選擇了MVC,有什麼優勢缺點,咱們怎麼利用和規避,讓UE的Controller們盡責的爲咱們服務,少形成麻煩。
對於簡單的遊戲或者引擎來講,有時並不須要把這三者分的很清,如Cocos2dx就沒有Controller,它的MVC就是混雜在一塊兒,由於代碼量少因此也還算勉強能湊合;Unity的MonoBehavior其實也至關於把MC放在了一塊兒,用得方便的同時也得當心太順手了出現組件之間互相網狀引用一團亂麻的狀況;UE在這個問題上的思考就有些一脈相承,既然Actor們形形色色,咱們以前也談過甚至有AInfo這種書記官,那爲什麼不讓一些Actor專門來承載邏輯呢?因而,Actor再度分化出Controller。下面咱們就來一一介紹Actor旗下Controller家族的指揮官們。設計模式

AController

雖然我在以前已經一再的劇透過AController是繼承自AActor的一個子類,可是爲了更好理解思考UE裏的Controller機制,請先把腦殼放空,也別去偷看UE裏的源碼,像張無忌同樣暫時忘記AController這回事,問本身一個問題:若是我想實現一種機制去控制遊戲裏的Actor,該怎麼設計?
巧婦難爲無米之炊,我們先來看看當前手上都有些什麼:數組

  1. UObject,反射序列化等機制
  2. UActorCompoent,功能的載體,必定程度的嵌套組裝能力(SceneComponent)
  3. AActor,基礎的遊戲對象,Component的容器
  4. APawn,分化出來的AActor,物理表示和基本的移動能力,當前正翹首以待。
  5. 沒了,在控制Actor這個層級,咱們還暫時不須要去考慮Level等更高層次的對象

針對APawn,再想一想咱們但願達成的控制願景,沒事,你儘管放開想象的想,作不作獲得我們先放一邊,但至少別在一開始就被想象力限制住了。「控制」自己雖然只是一段邏輯算法代碼,可是它也須要有個載體去承載和運行,某種意義上來講也算得上是個實體。因此下面咱們不妨就腦洞大開,以「控制」這個實體的視角口吻,講講「我,做爲一個——控制」但願擁有哪一些本領:服務器

  1. 可以和Pawn對應起來,理想狀況下,極端的靈活性應該是多對多。我但願我能同時控制多個Pawn,固然,一個Pawn也能夠被多個個人兄弟姐妹們一塊兒控制。想一想那些RTS遊戲和多人協做遊戲,你應該能明白我有時候須要協調調度Pawn們走個方陣,有時候也得多人合做才能操縱得了一臺機甲。固然越靈活也每每意味着越容易出錯,但總之咱們須要一個和Pawn關聯的機制。
  2. 多個控制實例,在須要的時候,我不介意能夠克隆出多個我來,好比一段邏輯A,咱們但願能夠有多個實例在同時運行。就像行爲樹同樣,能夠有多個運行實例,彼此算法同樣,但互不干擾。
  3. 可掛載釋放,我能夠選擇當前控制PawnA,也能夠選擇以後把它釋放掉再也不控制讓她自生自滅,而後再另尋新歡控制PawnB,我必須擁有靈活的運行時增刪控制Pawn的能力。
  4. 可以脫離Pawn存在,我思故我在,就算當前沒有任何Pawn控制,我也能夠繼續存在,這樣我就能夠延時動態的選擇Pawn對象。有些Pawn值得我去等。
  5. 操縱Pawn生死的能力,誰規定必須必定去控制世界當前存在的Pawn才行。當世界裏沒有Pawn可供我控制時,我但願能夠本身造一個出來。你要說她是玩具、亦或傀儡也好,我不在意。有時候我很羨慕暗黑裏的沉淪魔巫師,身邊老是圍繞着一羣沉淪魔,一個沉淪魔掛了,他能夠緊接着再復活一個出來,這樣永遠都不會感動寂寞,你說多好?那索性再霸道一點吧,要是我這個控制實體不在了,我但願能夠選擇是否帶Pawn們跟我一塊兒走,沒了我,她們都傻得讓人心疼。固然若是有哪一個Pawn能讓我這個霸道總裁愛上,我也願意陪她一塊兒去死。
  6. 根據配置自動生成,我(控制)雖然只是一段代碼,但也不能無中生有,因此也得有個機制能夠生成我這個控制實體,不過想來這應該是組織裏更上層領導的事,但至少他應該知道怎麼建立我出來。
  7. 事件響應,遊戲事件的一些控制關心的事件應該可以傳到我這裏,我能夠酌情處理。一樣,Pawn也能夠向我彙報,我會好好研究決定的,嗯。
  8. 持續的運行,沒事的時候,我喜歡聽世界大鐘的每一次Tick,跟個人心跳同步起來,就彷彿真的活過來同樣,能夠自主的作一些我想作的事,這是我最自在的時候。
  9. 自身有狀態,你累了要休息,我也同樣。我能夠選擇自身的狀態,選擇工做或者是休息,也能夠選擇今天是哪一個Pawn和心情最配。
  10. 擁有必定的擴展繼承組合能力,一方面我但願個人家族開枝散葉繁榮昌盛,個人一身本領繼承自個人父親,而我也將有個人兒,你們各有天賦。另外一方面,那些普通的Actor們均可以身背各個Component,更高貴的我固然也想有。
  11. 保存數據狀態,據說金魚的記憶只有7秒,但是我卻想記住你一生。因此我但願我能擁有一些記憶,人的過去成就瞭如今,也將指引着將來。之前有一我的跟我說過,當你不能再擁有的時候,惟一能作的就是令本身不要忘記。
  12. 可在世界裏移動,我能夠選擇賬中千里以外遙控Pawn,也能夠選擇附身在一個Pawn身上,這樣我才能多角度無死角的觀察我可愛的Pawn們,嘿嘿。
  13. 可探查世界的對象,我要有眼睛,讓我幹活,基本的我得看得見知道當前世界裏已經有哪些對象吧,不然不就抓瞎了嘛。
  14. 可同步,這年頭,要是不能適應網絡環境,可真的沒有競爭力。這個Object,Actor基本都有的能力,我固然也得有。位於服務器或客戶端上的我也必須有能力在其餘客戶端上影分身,讓他們都跟隨個人步伐一致行動。

在仔細考察了"控制"的需求和手頭上的原料以後,咱們試着從UE的角度來權衡一下。
首先Controller不能是一個Component,一是由於Component的層級過低,表達的是功能的概念而非邏輯;二是Component必須依附於Actor存在,而咱們的Controller但願能獨立存在。
其次若是從UObject直接繼承下來UController,卻是也可行,UObject也能複製同步,其餘的控制Pawn的能力和事件響應等倒也是能改改接口想一想辦法,可是要想在世界裏移動,就得有個位置表示,再加上還但願能容納Components,這就麻煩了,基本就是把Actor的工做再作一套,有點累人,搞起來也怕兩套班子出錯鬧矛盾。
再來考察下從AActor繼承下來AController怎麼樣,Actor比Object多了一些咱們正須要的配置動態生成、輸入事件響應、Tick、可繼承、可容納Component、可在世界裏出現、可在網絡間同步。好了,如今就差控制Pawn的能力,那咱們就在這個分化出來的AController增長一些控制Pawn的接口,這個思路正是和咱們從Actor從分化出Pawn的時候不謀而合!如今咱們來看看UE裏的AController:
AController.png-122.6kB
跟咱們的設計八九不離十,咱們再一一仔細驗證一番:
關聯Pawn的能力,有Possess和UnPossess,源碼裏也有PawnPendingDestroy等這些函數(未一一列出);GameMode中也保存着AIControllerClass和PlayerControllerClass的配置,用於在適當的時候Spanw出Controller;繼承於Actor也就有了EnableInput和Tick;Controller自己還能夠繼續派生下去(如AIController和PlayerController),也能夠容納Components;也帶着一個SceneComponent因此能夠擺放在世界中;自身也能夠添加成員變量來記憶存儲遊戲狀態;自身也有一個FName StateName(Playing、Spectating、Inactive),切換自身的狀態(運行,觀察,非激活);由於跟Pawn是平級的關係,只在運行的時候引用關聯,因此對彼此獨立存在不作強制約束,提升了靈活性。一個Pawn自身上也能夠配置策略:網絡

namespace EAutoReceiveInput
{
    enum Type
    {
        Disabled,
        Player0,
        Player1,
        Player2,
        Player3,
        Player4,
        Player5,
        Player6,
        Player7,
    };
}
TEnumAsByte<EAutoReceiveInput::Type> AutoPossessPlayer;

enum class EAutoPossessAI : uint8
{
    /** Feature is disabled (do not automatically possess AI). */
    Disabled,
    /** Only possess by an AI Controller if Pawn is placed in the world. */
    PlacedInWorld,
    /** Only possess by an AI Controller if Pawn is spawned after the world has loaded. */
    Spawned,
    /** Pawn is automatically possessed by an AI Controller whenever it is created. */
    PlacedInWorldOrSpawned,
};
EAutoPossessAI AutoPossessAI;
TSubclassOf<AController> AIControllerClass;

這樣在運行時UE也能夠根據Pawn建立配套的Controller。畢竟只是爲了闡明概念,而不是糾結技術細節,我對Controller的功能接口都只是粗略帶過,若是讀者本身去看Contoller的UE源碼,順即可以對我當前說的概念驗證一下,還會發現一些Movement和ViewPoint的接口,這些也算是和控制移動和視角配套吧。架構

思考:Controller和Pawn必須1:1嗎?
觀察UE實現裏咱們發現Controller裏只是保存了一個Pawn指針,而不是數組,這和一開始但願的多對多關係有些出入。理想和現實老是有差距,一個願景落實到工程實踐上也難免得有一些妥協。首先咱們再來梳理理解一下這個Possess(擁有/佔用)的概念。一個Controller能靈活的Possess/UnPossess一個Pawn,雖然一次只能控制一個,但在遊戲中咱們也能夠在不一樣的Pawn中切換,好比操縱一個主角坐進而後控制一輛汽車,或者端起固定的機關槍掃射,這些功能琢磨一下其實只是涉及操做實體Pawn的變化。若是咱們能妥善的用好Pawn和Controller的切換功能,大部分基本的遊戲功能也是可以比較方便的實現的。那麼有哪些是不太適合的呢?UE官方其實也認可了,見Controller文檔說明:mvc

By default, there is a one-to-one relationship between Controllers and Pawns; meaning, each Controller controls only one Pawn at any given time. This is acceptable for most types of games, but may need to be adjusted as certain types of games - real-time strategy comes to mind - may require the ability to control multiple entities at once.框架

對於RTS這種須要一會兒控制多個單位的遊戲來講,這種1v1的關係確實比較僵硬,就須要在Controller裏本身實現擴展一下,額外保存多個Pawn,而後本身實現一些須要的控制實現,但整體上也只能說得繞一下,也算不上特別複雜,因此就也不能說UE作不了某一些類型的遊戲,Epic是個遊戲引擎公司,賣的畢竟是個通用遊戲引擎。
OK,那UE爲什麼不實現成多對多呢?我以爲理由每每很簡單,就是想保持必定的簡單。遊戲引擎的每一個模塊的設計,甚至函數接口的設計,無時無刻不在權衡決定。太簡單了概念清晰用起來方便可是靈活擴展力不足,太靈活擴展無限了每每也會讓人無從適從容易出錯。當前1:1的時候,咱們的腦殼邏輯很清晰,咱們能夠在Controller裏直接GetPawn,也能夠在Pawn中GetController,都很是方便。調試邏輯Bug的時候,咱們也能很快找到查錯的目標。而對比想象,若是是M:N,靈活性是滿滿了,可是你能輕易的說出當前Pawn是被哪些Controller控制嗎?你也得時時記着這個Controller當前控制了哪些Pawn。OMG!這些Pawn和Controller多對多的構成了網狀結構,項目越龐大複雜,這張網也越能套住你。再從另外一個方面說,一旦提供了這種多對多的直接支持,以咱們人類的性格,免費現成的東西,咱們老是傾向於去找機會能用上它,而不是去琢磨到底應不該該用。因此一旦就這麼直接提供了,對於剛入門的新手,壓根就沒什麼指引,怎麼來好像均可以,就很是容易收不住把項目邏輯關係搞得沒必要要的複雜。因此之後UE就算想在這一方面優化增強,應該也會比較剋制。
索性再聊開一些,咱們用Unity來作一下對比。Unity就是GameObject+Component,你本身組合去吧,很是的靈活自由,也不作什麼限制,但形成的後果就是經常各類Component互相引用來引用去,網狀互聯一團亂麻。另外幾乎每一個人均可以在上面搞出一套遊戲系統出來,互相之間都是自成一派。因此常常網上就會有各類帖子問怎麼在Unity中實現MVC模式的,也有分析爐石傳說遊戲邏輯框架的。Unity固然是個好引擎,目前來講熱度也是比UE要高一些,但咱們也不能由於它火用得人多,就權威崇拜從衆的認爲Unity各個方面都比別的引擎好。設計架構遊戲的時候,工程師們要抵擋住靈活性的誘惑,保持克制每每是更可貴珍貴的美德。要認識到,引擎的終極目的是方便人使用的,咱們程序員每每很容易太沉迷於程序功能的靈活強大,而疏忽了易用性魯棒性等社會工程需求。

思考:爲什麼Controller不能像Actor層級嵌套?
咱們都知道Actor能夠藉着身上的SceneComponent互相嵌套。那麼AController一樣也是Actor,爲什麼不也實現這麼一個父子機制?從功能上來講,一個Controller能夠有子Controllers,聽起來也是很是靈活強大啊。可是冷靜想一下,Controller表達的「控制」的概念,因此在這裏你實際上想要表達的是一種「控制」互相嵌套的概念,感受又給「控制」給分了層,有「大控制」,也有「小控制」,可是「控制」的「大小」又是個什麼概念呢?咱們應該怎麼劃分控制的大小?「控制」本質上來講就是一些代碼,無論怎麼設計,目的都是用來表達遊戲遊戲邏輯的。而針對遊戲邏輯的複雜,怎麼更好的管理組織邏輯代碼,咱們有狀態機,分層狀態機,行爲樹,GOAL(目標導向),甚至你還能搞些神經網絡遺傳算法機器學習啥的。因此在咱們已經有這麼多工具的基礎上,徒增複雜性是很危險的作法。若是有必要,也能夠把Controller自己再看成其餘AI算法的容器,因此就不必在對象層次上再作文章了。

思考:Controller能夠顯示嗎?
既然Actor自己能夠帶着Mesh組件來渲染顯示,那Controller可不能夠呢?是否是Controller都是不可見的?這個答案可說是也能夠說不是,由於Controller自己確實就是一個特殊點的Actor而已,你依然能夠在Controller中添加Mesh組件,添加別的子Actor等,因此從這個方面說Controller是有能夠渲染顯示的能力的。可是一個控制者畢竟只是表達一個邏輯的概念,因此爲了分工明確,UE就乾脆在Controller的構造函數裏把本身給隱藏了:

bHidden = true;
#if WITH_EDITORONLY_DATA
    bHiddenEd = true;
#endif // WITH_EDITORONLY_DATA

事了拂衣去,深藏功與名。爲了驗證個人說法,讀者你能夠親自在PlayController下掛一些Cube之類的Actor,而後在源碼層把這兩個值改成false,從新編譯運行看下結果,看可否正確顯示出來,這裏我就不貼圖了,很好玩的哦。

思考:Controller的位置有什麼意義?
既然Controller自己只是控制者,那它在場景中的位置和移動有什麼意義嗎?Controller爲什麼還須要個SceneComponent?意義在於若是Controller自己有位置信息,就能夠利用該信息更好的控制Pawn的位置和移動。
首先說下Controller的Rotation,這個比較好理解一點,若是我想讓個人Pawn和Controller保持旋轉朝向一致,由於是Controller做主控制Pawn的關係,因此Controller就得維護本身的Rotation。再來講位置,若是Controller有本身的位置,這樣在Respawn從新生成Pawn的時候,你就能夠選擇在當前位置建立。所以爲了自動更新Controller的位置,UE還提供了一個bAttachToPawn的開關選項,默認是關閉的,UE不會自動的更新Controller的位置信息;而若是打開,就會把Controller附加到Pawn的子節點裏面去,讓Controller跟隨Pawn來移動。你能夠把這兩種模式想象成一種是上帝視角在千里以外心電感應控制Pawn,另外一種是騎在Pawn肩上來指揮。
固然若是這個Controller確實只是純樸的邏輯控制的話(如AIController),那確實位置也沒什麼意義。因此UE甚至還隱藏了Controller的一些更新位置的接口,儘可能避免讓人手動去操縱:

private:
    // Hidden functions that don't make sense to use on this class.
    HIDE_ACTOR_TRANSFORM_FUNCTIONS();
//展開後:
//////////////////////////////////////////////////////////////////////////
// Macro to hide common Transform functions in native code for classes where they don't make sense.
// Note that this doesn't prevent access through function calls from parent classes (ie an AActor*), but
// does prevent use in the class that hides them and any derived child classes.

#define HIDE_ACTOR_TRANSFORM_FUNCTIONS() private: \
    FTransform GetTransform() const { return Super::GetTransform(); } \
    FVector GetActorLocation() const { return Super::GetActorLocation(); } \
    FRotator GetActorRotation() const { return Super::GetActorRotation(); } \
    FQuat GetActorQuat() const { return Super::GetActorQuat(); } \
    FVector GetActorScale() const { return Super::GetActorScale(); } \
    bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr) { return Super::SetActorLocation(NewLocation, bSweep, OutSweepHitResult); } \
    bool SetActorRotation(FRotator NewRotation) { return Super::SetActorRotation(NewRotation); } \
    bool SetActorRotation(const FQuat& NewRotation) { return Super::SetActorRotation(NewRotation); } \
    bool SetActorLocationAndRotation(FVector NewLocation, FRotator NewRotation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr) { return Super::SetActorLocationAndRotation(NewLocation, NewRotation, bSweep, OutSweepHitResult); } \
    bool SetActorLocationAndRotation(FVector NewLocation, const FQuat& NewRotation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr) { return Super::SetActorLocationAndRotation(NewLocation, NewRotation, bSweep, OutSweepHitResult); } \
    virtual bool TeleportTo( const FVector& DestLocation, const FRotator& DestRotation, bool bIsATest, bool bNoCheck ) override { return Super::TeleportTo(DestLocation, DestRotation, bIsATest, bNoCheck); } \
    virtual FVector GetVelocity() const override { return Super::GetVelocity(); } \
    float GetHorizontalDistanceTo(AActor* OtherActor)  { return Super::GetHorizontalDistanceTo(OtherActor); } \
    float GetVerticalDistanceTo(AActor* OtherActor)  { return Super::GetVerticalDistanceTo(OtherActor); } \
    float GetDotProductTo(AActor* OtherActor) { return Super::GetDotProductTo(OtherActor); } \
    float GetHorizontalDotProductTo(AActor* OtherActor) { return Super::GetHorizontalDotProductTo(OtherActor); } \
    float GetDistanceTo(AActor* OtherActor) { return Super::GetDistanceTo(OtherActor); }

UE這裏其實想說的是,這些更新位置的操做仍是讓我來爲你管理吧,我真的擔憂你會用錯搞出什麼亂子來。順便再說些題外話,對於PlayerController來講,由於玩家須要一個視角來觀察世界,因此經常PlayerController經常會扛着個攝像機出現(藍圖裏沒有,可是會運行時生成PlayerCameraManager和CameraActor),因此就算沒有角色可供操做,玩家也依然但願是能夠視角漫遊觀察整個世界的(試試看把默認Level裏的PlayerStart刪掉後運行看看)。這個時候PlayerController經常會默認建立出一個ASpectatorPawn或者DefaultPawn(根據GameMode裏配置),咱們雖然看不見Pawn,但依然能夠觀察世界,靠得就是跟Controller關聯的旋轉和攝像機。

思考:哪些邏輯應該寫在Controller中?
如同當初咱們在思考Actor和Component的邏輯劃分同樣,咱們也得要劃分哪些邏輯應該放在Pawn中,哪些應該放在Contrller中。上文咱們也說過,Pawn也能夠接收用戶輸入事件,因此其實只要你願意,你甚至能夠脫離Controller作一個特立獨行的Pawn。那麼在那些時候須要Controller?哪些邏輯應該由Controller掌管呢?能夠從如下一些方面考慮:

  • 從概念上,Pawn自己表示的是一個「能動」的概念,重點在於「能」。而Controller表明的是動到「哪裏」的概念,重點在於「方向」。因此若是是一些Pawn自己固有的能力邏輯,如前進後退、播放動畫、碰撞檢測之類的就徹底能夠在Pawn內實現;而對於一些可替換的邏輯,或者智能決策的,就應該歸Controller管轄。
  • 從對應上來講,若是一個邏輯只屬於某一類Pawn,那麼其實你放進Pawn內也挺好。而若是一個邏輯能夠應用於多個Pawn,那麼放進Controller就能夠組合應用了。舉個例子,在戰爭遊戲中,假設說有坦克和卡車兩種戰車(Pawn),只有坦克能夠開炮,那麼開炮這個功能你就能夠直接實如今坦克Pawn上。而這兩輛戰車都有的自動尋找攻擊目標功能,就能夠實如今一個Controller裏。
  • 從存在性來講,Controller的生命期比Pawn要長一些,好比咱們常常會實現的遊戲中玩家死亡後復活的功能。Pawn死亡後,這個Pawn就被Destroy了,就算以後再Respawn建立出來一個新的,可是Pawn身上保存的變量狀態都已經被重置了。因此對於那些須要在Pawn以外還要持續存在的邏輯和狀態,放進Controller中是更好的選擇。

APlayerState

咱們上文提到過Controller但願也能有一些記憶,保存住一些遊戲狀態。那麼到底應該怎麼保存呢?AController自身固然能夠添加成員變量來保存,這些變量也能夠網絡複製,通常來講也夠用。可是終究仍是遺忘了一個最重要的數據狀態。整個遊戲世界構建起來就是爲了玩家服務的,而玩家在遊戲過程當中,確定要存取產生一些狀態。而Controller做爲遊戲業務邏輯最重要的載體,勢必要和玩家的狀態打交道。因此Controller若是能夠動態存取玩家的狀態就會大爲方便了。所以咱們會在Controller中見到:

/** PlayerState containing replicated information about the player using this controller (only exists for players, not NPCs). */
    UPROPERTY(replicatedUsing=OnRep_PlayerState, BlueprintReadOnly, Category="Controller")
    class APlayerState* PlayerState;

而APlayerState的繼承體系是:
APlayerState.png-9.9kB
至於爲啥APlayerState是從AActor派生的AInfo繼承下來的,咱們聰明的讀者相信也能猜獲得了,因此也就不費口舌論證了。無非就是貪圖AActor自己的那些特性以網絡複製等。而AInfo們正是這種不愛表現的純數據書呆子們的大本營。而這個PlayerState咱們能夠經過在GameMode中配置的PlayerStateClass來自動生成。
注意,這個APlayerState也理所固然是生成在Level中的,跟Pawn和Controller是平級的關係,Controller裏只不過保存了一個指針引用罷了。註釋裏說的PlayerState只爲players存在,不爲NPC生成,指的是PlayerState是跟UPlayer對應的,換句話說當前遊戲有多少個真正的玩家,纔會有多少個PlayerState,而那些AI控制的NPC由於不是真正的玩家,因此也不須要建立生成PlayerState。可是UE把PlayerState的引用變量放在了Controller一級,而不是PlayerController之中,說明了其實AIController也是能夠設置讀取該變量的。一個AI智能可以讀取玩家的比分等狀態,有了更多的信息來做決策,想來也沒有什麼不對嘛。
Controller和網絡的結合很緊密,不少機制和網絡也很是強關聯,可是在這裏並不詳細敘述,這裏先能夠單純理解成Controller也能夠看成玩家在服務器上的代理對象。把PlayerState獨立構成一個Actor還有一個好處,當玩家偶爾因網絡波動斷線,由於這個鏈接不在了,因此該Controller也失效了被釋放了,服務器能夠把對應的該PlayerState先暫存起來,等玩家再緊接着重連上了,能夠利用該PlayerState從新掛接上Controller,以此提供一個比較順暢無縫的體驗。至於AIController,由於都是運行在Server上的,Client上並無,因此也就無所謂了。

思考:哪些數據應該放在PlayerState中?
從應用範圍上來講,PlayerState表示的是玩家的遊玩數據,因此那些關卡內的其餘遊戲數據就不該該放進來(GameState是個好選擇),另外Controller自己運行須要的臨時數據也不該該歸PlayerState管理。而玩家在切換關卡的時候,APlayerState也會被釋放掉,全部PlayerState實際上表達的是當前關卡的玩家得分等數據。這樣,那些跨關卡的統計數據等就也不該該放進PlayerState裏了,應該放在外面的GameInstance,而後用SaveGame保存起來。

總結

在遊戲裏,若是要評勞模,那Controller們無疑是最兢兢業業的,雖然有時候蠻橫霸道了一些,可是常常工做在第一線,下面的Pawn們經常智商過低,上面的Level,GameMode們又有點高高在上,讓他們直接管理數量繁多的Pawn們又有點太折騰,因而事無鉅細的真正幹那些髒活累活的還得靠Controller們。本文雖然沒有在網絡一塊留太多筆墨,可是Controller也是同時做爲聯機環境中最重要的溝通渠道,身兼要職。
回顧總結一下本文要點,UE在Pawn這個層級演化構成了一個最基本和很是完善的Component-Actor-Pawn-Controller的結構:
ControllerAndPawn.png-60.9kB
經過分化出來後的Actor的互相控制,既充分利用了現有的機制功能,又提供了足夠的靈活性,並且作的更改還不多,不用再設計額外另外一套框架。讀者朋友們,如今咱們若是翻到第一小節,想一想UE最初從Object分化出Actor的那一刻,是否是有不少感慨和感動呢?一個最初的很簡單的遊戲對象表示,慢慢演化派生充實起來,彼此之間通力配合,竟也能優雅的運轉起來。

有時候架構的設計和搭建是一脈相承的,最初的時候選擇了什麼樣的模型和骨架,後面再設計別的邏輯框架等其餘模塊,也基本上都得跟最初的設計配合着來。因此有時候每每也會發現,怎麼感受我架構設計的方案可選擇數量並很少啊?實際上是由於若是一開始鋪墊的好,接下來的設計水到渠成天然而然,讓你感受不到用心設計的力氣。UE以Actor的視角來看待世間萬物,天然獲得的是一個Actor繁榮昌盛的世界;Unity以Component來組裝萬物,獲得的就是個各類插件組件組裝出的世界;而若是如Cocos2dx通常萬物都是Node,那麼天然也會獲得一棵掛滿各類Node的世界之樹。這也算是遊戲引擎的基因吧。

本想着一篇介紹完Controller、PlayerController和AIController這三個對象,可是Controller自己是UE裏極爲重要的核心概念,自身的功能很是的豐富,牽扯的模塊也比較多,所以想抽離闡述最核心的概念和功能並非一件容易的事。花了這麼長的篇幅,只討論揣摩了Controller的設計過程和最基本的職責(還有輸入網絡等都沒有解釋),順便先簡單介紹了下PlayerState出場(PlayerState其實是跟UPlayer關聯更大一些,PlayerController等後續章節會繼續討論它),對於PlayerController和AIController,目前也只是語焉不詳的含糊帶過。不過仍是但願讀者們能從中吸收到設計的養分,把握清楚概念了,才能更好的組織遊戲邏輯,開發出更好的遊戲。

本系列教程的一個重點也是嘗試介紹引擎各類概念背後的考量,而不是單純的敘述解釋各個模塊功能。筆者始終認爲,只有咱們願意不吝口舌的去討論,願意耐下心來去思考學習,這些概念的領悟纔會瞭然在心中。不然若只是單純的介紹Pawn功能有123,Controller能夠ABC,相信讀者在閱讀完以後也並不會有什麼深的印象,由於這些只是設計的結果,少了設計的過程。

而下篇咱們將隆重介紹Controller家族中最耀眼的明星、上帝的寵兒:PlayerController!

引用

  1. Controller

UE4的版本更新實在太快,爲了留下版本存照和供讀者查證,之後在篇尾都會標註上本文研究使用的源碼版本。之後再也不特地作此聲明。
UE 4.13.2


知乎專欄:InsideUE4

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

我的原創,未經受權,謝絕轉載!

相關文章
相關標籤/搜索