引言

上文我們談到了Component-Actor-Pawn-Controller的結(jié)構(gòu),追溯了AController整個家族的崛起和身負(fù)的使命。本篇我們繼續(xù)來探討Controller家族中最為人所知的PlayerController和AIController。
作為一個Controller,我們討論的依然是該如何控制。我們已經(jīng)知道了Controller可以Possess并控制Pawn,但是Controller本身又是怎么驅(qū)動起來的呢?一個游戲里的控制角色大抵都可以分為兩類:玩家和AI。不管是單機(jī)游戲或者分屏多玩家,還是網(wǎng)絡(luò)玩家聯(lián)機(jī)對戰(zhàn),游戲都是為了玩家服務(wù)的,所以也必然會有一個或多個玩家,就算是如《山》那種純看的游戲,也是有一個“可觀察不可動”的玩家的。而AI的實(shí)體的數(shù)量就可以是零或者多個。
Note1:依舊重申:輸入、網(wǎng)絡(luò)、AI行為樹等模塊雖跟PlayerController和AIController關(guān)系緊密,但目前都暫且不討論,留待各自模塊章節(jié)再敘述。

APlayerController

讓咱們先從簡單的單機(jī)游戲開始討論吧,比如一款單機(jī)FPS游戲,這個游戲里已經(jīng)用各種各樣的Actor們構(gòu)建完成了世界場景,你的主角和敵人Pawn們也都在整裝待發(fā),這個時候你思考這么一個問題,我該怎么玩這個游戲?壯麗的舞臺已經(jīng)準(zhǔn)備好了,就等你入場了。先拋開具體的引擎而言,首先你需要能看見(擁有Camera和位置),其次你必須能響應(yīng)輸入(玩家按WASD你應(yīng)該能接收到),然后你可以根據(jù)輸入操控一些Pawn(Possess然后傳遞Input),這樣一個單機(jī)游戲中的簡單玩家控制器就差不多了。一個游戲中只有一個PlayerController,在不同的關(guān)卡中你可以使用不同的PlayerController,但是同一時刻響應(yīng)的只能是一個PlayerController。
插上多個手柄,咱們再拓展一下,比如像《街霸》那種單PC但是多玩家對抗或者協(xié)作的游戲。兩個玩家可以分別用兩個手柄,或者一個用鍵盤一個用鼠標(biāo),甚至是鍵盤上的不同區(qū)域,形式可以多種多樣。這個時候如果依然只有一個PlayerController,實(shí)現(xiàn)起來其實(shí)也是可行的,把兩個手柄——所有的輸入都由這個PlayerController來接收,然后在PlayerController內(nèi)部再分別根據(jù)情況去處理不同的Pawn。但是這種方式的缺點(diǎn)顯然也在于很容易把玩家1、2的輸入和控制混雜在一起,沒有清晰的區(qū)分開。因此,為了支持這種情況,我們可以開始允許游戲中同時出現(xiàn)多個PlayerController,每個PlayerController甚至都可以擁有自己的Viewport(分屏或者不同窗口),這樣我們通過配置,可以精確的路由手柄1的輸入給玩家1,各自的邏輯也很好的區(qū)分和復(fù)用。
再插上網(wǎng)線繼續(xù),到了網(wǎng)游時代,我們的游戲就開始允許有多人聯(lián)機(jī)對戰(zhàn)了。玩家在自己的PC上控制的只是自己的本地的角色,而屏幕游戲里其他的玩家角色是由網(wǎng)線另一端的玩家控制的。為了更好的適應(yīng)這種情況,我們就又得擴(kuò)展一下PlayerController的概念,PlayerController不僅能控制本地的Pawn,而且還能“控制”遠(yuǎn)程的Pawn(實(shí)際上是通過Server上的PlayerController控制Server上的Pawn,然后再復(fù)制到遠(yuǎn)程機(jī)器上的Pawn實(shí)現(xiàn)的)。
因此我們來看看UE里的PlayerController:

PlayerController因?yàn)槭侵苯痈婕掖蚪坏赖倪壿嬵?,因此是UE里使用最多的類之一。UE4.13.2版本里1632行的.h文件和4686行的.cpp文件,里面實(shí)現(xiàn)了很多的功能,初閱讀起來往往深陷其中不得要領(lǐng)。但是在上述的分析了之后,我們也可以在其中大概歸納出幾個模塊:

  • Camera的管理,目的都是為了控制玩家的視角,所以有了PlayerCameraManager這一個關(guān)聯(lián)很緊密的攝像機(jī)管理類,用來方便的切換攝像機(jī)。PlayerController的ControlRotation、ViewTarget等也都是為了更新Camera的位置。因?yàn)楦鶦amera的關(guān)系緊密,而Camera最后輸出的是屏幕坐標(biāo)里的圖像,所以為了方便一些拾取的HitResult函數(shù)也都是實(shí)現(xiàn)在這里面。渲染章節(jié)會再詳細(xì)介紹UE的攝像機(jī)管理。

  • Input系統(tǒng),包括構(gòu)建InputStack用來路由輸入事件,也包括了自己對輸入事件的處理。所以包含了UPlayerInput來委托處理。

  • UPlayer關(guān)聯(lián),既然顧名思義是PlayerController,那自然要和Player對應(yīng)起來,這也是PlayerController最核心的部分。一個UPlayer可以是本地的LocalPlayer,也可以是一個網(wǎng)絡(luò)控制UNetConnection。PlayerController只有在SetPlayer之后,才可以開始正常工作。

  • HUD顯示,用于在當(dāng)前控制器的攝像機(jī)面前一直顯示一些UI,這是從UE3遷移過來的組件,現(xiàn)在用UMG的比較多,等介紹UI模塊的時候再詳細(xì)介紹。

  • Level的切換,PlayerController作為網(wǎng)絡(luò)里通道,在一起進(jìn)行Level Travelling的時候,也都是先通過PlayerController來進(jìn)行RPC調(diào)用,然后由PlayerController來轉(zhuǎn)發(fā)到自己World中來實(shí)際進(jìn)行。

  • Voice,也是為了方便網(wǎng)絡(luò)中語音聊天的一些控制函數(shù)。

簡單來說,PlayerController作為玩家直接控制的實(shí)體,很多的跟玩家直接相關(guān)的操作也都得委托它來完成。目前來說PlayerController里旗下的100+的函數(shù)也大概可以分為以上幾大模塊,也根據(jù)需要重載了Controller里的一些其他函數(shù)。
UE的思想是具象化一個“玩家實(shí)體”,并把所有的跟該玩家相關(guān)的操作和接口都交給它完成。一般其他的游戲引擎只是個“功能引擎”,提供了一些圖形渲染UI系統(tǒng)等組件,但是在GamePlay這個層次就都非常欠缺了,一般都需要開發(fā)者自己搭建一套。而回想你寫過的游戲,是不是也往往有一個Player類(一般是單件或者全局變量)?里面幾乎是放著所有跟該玩家相關(guān)的業(yè)務(wù)邏輯代碼。UE里的PlayerController就是這種概念,優(yōu)點(diǎn)當(dāng)然是直接方便好理解,缺點(diǎn)也如你所見,會代碼膨脹得比較快。不過目前來說還算能接受,等某一塊功能真的比較大了之后,可以再把它抽出一個單獨(dú)的類來,如PlayerInput和PlayerCameraManager一樣。

思考:哪些邏輯應(yīng)該放在PlayerController中?
回想我們上篇的問題:“哪些邏輯應(yīng)該寫在Controller中?”,該處的答案觀點(diǎn)在本處也依然適用。不過我還想再補(bǔ)充幾點(diǎn):

  • 對實(shí)現(xiàn)游戲邏輯來說,如果是按照MVC的視角,那么View對應(yīng)的是Pawn的表現(xiàn),而PlayerController對應(yīng)的是Controller的部分,那Model就是游戲業(yè)務(wù)邏輯的數(shù)據(jù)了。拿超級馬里奧游戲來舉例子,把問題先局限在一個關(guān)卡內(nèi),假設(shè)要實(shí)現(xiàn)的是金幣的邏輯,那么View指的是游戲右上角的金幣數(shù)目UI,而玩家用PlayerController來控制馬里奧來蹦跳行走,而馬里奧(Pawn)通過觸碰金幣的事件又上報(bào)給PlayerController來相應(yīng)增加金幣。而PlayerController存儲金幣的數(shù)據(jù)就是在PlayerState中。即PlayerState中有一個int coin,也有相應(yīng)的AddCoin(int coin)。而PlayerController的職責(zé)應(yīng)該是一邊控制Pawn,一邊負(fù)責(zé)內(nèi)部正確的調(diào)用PlayerState的Coin接口。那么PlayerController里的成員變量有什么用?根據(jù)單一職責(zé)原則,我們寫在哪個類里的變量應(yīng)該盡量只符合該類的作用,所以PlayerController里的變量的意義在于更好的實(shí)現(xiàn)控制。比如假設(shè)玩家在一個關(guān)卡內(nèi)可以按AABB來作弊獲得100金幣,但是限最多3次。那么這個按鍵的響應(yīng)就應(yīng)該由PlayerController來接收,然后調(diào)用AddCoin(100),并更新PlayerController里的成員變量CoinCheatCount。也或者想實(shí)現(xiàn)馬里奧的加速跑,也可以在PlayerController里增加Speed的成員變量。

  • 記住PlayerController是可被替換的,不同的關(guān)卡里也可能是不一樣的。比如馬里奧在水下的時候控制的方式明顯就不一樣,所以就不能像“Player”單件類那樣什么都往里面塞。這樣一旦被替換掉了之后數(shù)據(jù)就都丟失了。

  • PlayerController也不一定存在,考慮一下如果把馬里奧做成聯(lián)機(jī)游戲,那么對方玩家被同步過來的將只有PlayerState,對方玩家的PlayerController只在服務(wù)器上存在。所以這個時候,如果你把金幣數(shù)據(jù)放在PlayerController里的話就非常尷尬了。所以為了擴(kuò)展性來說,還是根據(jù)職責(zé)分明的原則來正確劃分業(yè)務(wù)邏輯會比較好。

  • 在任一刻,Player:PlayerController:PlayerState是1:1:1的關(guān)系。但是PlayerController可以有多個備選用來切換,PlayerState也可以相應(yīng)多個切換。UPlayer的概念會在之后講解,但目前可以簡單理解為游戲里一個全局的玩家邏輯實(shí)體,而PlayerController代表的就是玩家的意志,PlayerState代表的是玩家的狀態(tài)。

AAIController

從某種程度上來說,AI也可以算是一個Player,只不過它不需要接收玩家的控制,可以自行決策行動。從玩家控制的邏輯需要有一個載體一樣,AI的邏輯算法也需要有一個運(yùn)行的實(shí)體。而這就是UE里的AIController:

同PlayerController對比,少了Camera、Input、UPlayer關(guān)聯(lián),HUD顯示,Voice、Level切換接口,但也增加了一些AI需要的組件:

  • Navigation,用于智能根據(jù)導(dǎo)航尋路,其中我們常用的MoveTo接口就是做這件事情的。而在移動的過程中,因?yàn)樯倭送婕铱刂频膩磙D(zhuǎn)向,所以多了一個SetFocus來控制當(dāng)前的Pawn視角朝向哪個位置。

  • AI組件,運(yùn)行啟動行為樹,使用黑板數(shù)據(jù),探索周圍環(huán)境,以后如果有別的AI算法方法實(shí)現(xiàn)成組件,也應(yīng)該在本組件內(nèi)組合啟動。

  • Task系統(tǒng),讓AI去完成一些任務(wù),也是實(shí)現(xiàn)GameplayAbilities系統(tǒng)的一個接口。目前簡單來說GameplayAbilities是為Actor添加額外能力屬性集合的一個模塊,比如HP,MP等。其中的GamePlayEffect也是用來實(shí)現(xiàn)Buffer的工具。另外GamePlayTags也是用來給Actor添加標(biāo)簽標(biāo)記來表明狀態(tài)的一種機(jī)制。目前來說該兩個模塊似乎都是由Epic的Game Team在維護(hù),所以完成度不是非常的高,用的時候也往往需要根據(jù)自己情況去重構(gòu)調(diào)整。

本文重點(diǎn)不在于討論AI內(nèi)部的各種組件功能,因此我們先把目光聚焦在AIController對象本身上。同PlayerController一樣,AIController也只存在于Server上(單機(jī)游戲也可看作是Server)。游戲里必須有玩家參與,而AI可以沒有,所以AIController并不一定會存在。我們可以在Pawn上配置AIControllerClass來讓該P(yáng)awn產(chǎn)生的時候自動為它分配一個AIController,之后自動釋放。

思考:哪些邏輯應(yīng)該放在AIController中?
我們依然要思考這個問題,大部分思想和原則和PlayerController是一樣的,只不過AI算法的多種多樣,所以我們推薦盡量利用UE提供的行為樹黑板等組件實(shí)現(xiàn),而不是直接在AIController硬編碼再度實(shí)現(xiàn)。也請把目光僅僅局限在當(dāng)前的Pawn身上,不要在里面寫其他無關(guān)的邏輯。另外,因?yàn)锳IController都是在關(guān)卡內(nèi)比較短暫存在的,一般不太有垮Level的數(shù)據(jù)保存,所以你可以用AIController的成員變量來保存狀態(tài)。而如果真的需要用到PlayerController的狀態(tài),則也可以引用一個PlayerState過來。如果想引用關(guān)卡的全局狀態(tài),也可以引用GameState,再更高級別的,甚至可以直接和GameInstance接觸。
但是AIController也可以通過配置bWantsPlayerState來獲得自己的PlayerState,所以PlayerState其實(shí)也并不是跟UPlayer綁定的,畢竟從本質(zhì)上來說APlayerState也只是個AInfo(AActor),跟其他Actor一樣可以有多個,并沒有什么稀奇的,區(qū)別是你自己怎么創(chuàng)建并利用它。

總結(jié)

到此,我們也算討論完了Actor(Pawn)層次的控制,在這個層次上,我們關(guān)注的焦點(diǎn)在于如何更好的控制游戲世界里各種Actor交互和邏輯。UE采用了分化Actor的思維創(chuàng)建出AController來控制APawn們,因?yàn)橥婕彝嬗螒蛞踩际强刂浦螒蚶锏囊粋€化身來行動,所以UE抽象總結(jié)分化了一個APlayerController來上接Player的輸入,下承Pawn的控制。對于那些自治的AI實(shí)體,UE給予了同樣的尊重,創(chuàng)建出AIController,包含了一些方便的AI組件來實(shí)現(xiàn)游戲邏輯。并利用PlayerState來存儲狀態(tài)數(shù)據(jù),支持在網(wǎng)絡(luò)間同步。

上圖應(yīng)該可以比較清晰的闡明,UE是如何充分利用Actor的本身機(jī)制來反過來實(shí)現(xiàn)對Actor的邏輯控制,相信親愛的讀者朋友們也能自行體會到它的優(yōu)雅之處。對比其他的游戲引擎,往往它們都止步于Actor這一個層次,只提供了最基本的對象層次,美名其曰交給玩家控制。UE為我們提供了這一套簡潔強(qiáng)大的機(jī)制,大大方便了我們編寫邏輯的難度。

而下篇我們的邏輯之旅將再繼續(xù)拔高一個層次,將開始講解World層次的邏輯,這個世界的意志:GameMode!
下篇:GamePlay架構(gòu)(七)GameMode和GameState