PlayerController:你不懂,伴君如伴虎啊
AIController:上來,我自己動
引言
上文我們談到了Component-Actor-Pawn-Controller的結構,追溯了AController整個家族的崛起和身負的使命。本篇我們繼續來探討Controller家族中最為人所知的PlayerController和AIController。
作為一個Controller,我們討論的依然是該如何控制。我們已經知道了Controller可以Possess並控制Pawn,但是Controller本身又是怎么驅動起來的呢?一個游戲里的控制角色大抵都可以分為兩類:玩家和AI。不管是單機游戲或者分屏多玩家,還是網絡玩家聯機對戰,游戲都是為了玩家服務的,所以也必然會有一個或多個玩家,就算是如《山》那種純看的游戲,也是有一個“可觀察不可動”的玩家的。而AI的實體的數量就可以是零或者多個。
Note1:依舊重申:輸入、網絡、AI行為樹等模塊雖跟PlayerController和AIController關系緊密,但目前都暫且不討論,留待各自模塊章節再敘述。
APlayerController
讓咱們先從簡單的單機游戲開始討論吧,比如一款單機FPS游戲,這個游戲里已經用各種各樣的Actor們構建完成了世界場景,你的主角和敵人Pawn們也都在整裝待發,這個時候你思考這么一個問題,我該怎么玩這個游戲?壯麗的舞台已經准備好了,就等你入場了。先拋開具體的引擎而言,首先你需要能看見(擁有Camera和位置),其次你必須能響應輸入(玩家按WASD你應該能接收到),然后你可以根據輸入操控一些Pawn(Possess然后傳遞Input),這樣一個單機游戲中的簡單玩家控制器就差不多了。一個游戲中只有一個PlayerController,在不同的關卡中你可以使用不同的PlayerController,但是同一時刻響應的只能是一個PlayerController。
插上多個手柄,咱們再拓展一下,比如像《街霸》那種單PC但是多玩家對抗或者協作的游戲。兩個玩家可以分別用兩個手柄,或者一個用鍵盤一個用鼠標,甚至是鍵盤上的不同區域,形式可以多種多樣。這個時候如果依然只有一個PlayerController,實現起來其實也是可行的,把兩個手柄——所有的輸入都由這個PlayerController來接收,然后在PlayerController內部再分別根據情況去處理不同的Pawn。但是這種方式的缺點顯然也在於很容易把玩家1、2的輸入和控制混雜在一起,沒有清晰的區分開。因此,為了支持這種情況,我們可以開始允許游戲中同時出現多個PlayerController,每個PlayerController甚至都可以擁有自己的Viewport(分屏或者不同窗口),這樣我們通過配置,可以精確的路由手柄1的輸入給玩家1,各自的邏輯也很好的區分和復用。
再插上網線繼續,到了網游時代,我們的游戲就開始允許有多人聯機對戰了。玩家在自己的PC上控制的只是自己的本地的角色,而屏幕游戲里其他的玩家角色是由網線另一端的玩家控制的。為了更好的適應這種情況,我們就又得擴展一下PlayerController的概念,PlayerController不僅能控制本地的Pawn,而且還能“控制”遠程的Pawn(實際上是通過Server上的PlayerController控制Server上的Pawn,然后再復制到遠程機器上的Pawn實現的)。
因此我們來看看UE里的PlayerController:
PlayerController因為是直接跟玩家打交道的邏輯類,因此是UE里使用最多的類之一。UE4.13.2版本里1632行的.h文件和4686行的.cpp文件,里面實現了很多的功能,初閱讀起來往往深陷其中不得要領。但是在上述的分析了之后,我們也可以在其中大概歸納出幾個模塊:
- Camera的管理,目的都是為了控制玩家的視角,所以有了PlayerCameraManager這一個關聯很緊密的攝像機管理類,用來方便的切換攝像機。PlayerController的ControlRotation、ViewTarget等也都是為了更新Camera的位置。因為跟Camera的關系緊密,而Camera最后輸出的是屏幕坐標里的圖像,所以為了方便一些拾取的HitResult函數也都是實現在這里面。渲染章節會再詳細介紹UE的攝像機管理。
- Input系統,包括構建InputStack用來路由輸入事件,也包括了自己對輸入事件的處理。所以包含了UPlayerInput來委托處理。
- UPlayer關聯,既然顧名思義是PlayerController,那自然要和Player對應起來,這也是PlayerController最核心的部分。一個UPlayer可以是本地的LocalPlayer,也可以是一個網絡控制UNetConnection。PlayerController只有在SetPlayer之后,才可以開始正常工作。
- HUD顯示,用於在當前控制器的攝像機面前一直顯示一些UI,這是從UE3遷移過來的組件,現在用UMG的比較多,等介紹UI模塊的時候再詳細介紹。
- Level的切換,PlayerController作為網絡里通道,在一起進行Level Travelling的時候,也都是先通過PlayerController來進行RPC調用,然后由PlayerController來轉發到自己World中來實際進行。
- Voice,也是為了方便網絡中語音聊天的一些控制函數。
簡單來說,PlayerController作為玩家直接控制的實體,很多的跟玩家直接相關的操作也都得委托它來完成。目前來說PlayerController里旗下的100+的函數也大概可以分為以上幾大模塊,也根據需要重載了Controller里的一些其他函數。
UE的思想是具象化一個“玩家實體”,並把所有的跟該玩家相關的操作和接口都交給它完成。一般其他的游戲引擎只是個“功能引擎”,提供了一些圖形渲染UI系統等組件,但是在GamePlay這個層次就都非常欠缺了,一般都需要開發者自己搭建一套。而回想你寫過的游戲,是不是也往往有一個Player類(一般是單件或者全局變量)?里面幾乎是放着所有跟該玩家相關的業務邏輯代碼。UE里的PlayerController就是這種概念,優點當然是直接方便好理解,缺點也如你所見,會代碼膨脹得比較快。不過目前來說還算能接受,等某一塊功能真的比較大了之后,可以再把它抽出一個單獨的類來,如PlayerInput和PlayerCameraManager一樣。
思考:哪些邏輯應該放在PlayerController中?
回想我們上篇的問題:“哪些邏輯應該寫在Controller中?”,該處的答案觀點在本處也依然適用。不過我還想再補充幾點:
- 對實現游戲邏輯來說,如果是按照MVC的視角,那么View對應的是Pawn的表現,而PlayerController對應的是Controller的部分,那Model就是游戲業務邏輯的數據了。拿超級馬里奧游戲來舉例子,把問題先局限在一個關卡內,假設要實現的是金幣的邏輯,那么View指的是游戲右上角的金幣數目UI,而玩家用PlayerController來控制馬里奧來蹦跳行走,而馬里奧(Pawn)通過觸碰金幣的事件又上報給PlayerController來相應增加金幣。而PlayerController存儲金幣的數據就是在PlayerState中。即PlayerState中有一個int coin,也有相應的AddCoin(int coin)。而PlayerController的職責應該是一邊控制Pawn,一邊負責內部正確的調用PlayerState的Coin接口。那么PlayerController里的成員變量有什么用?根據單一職責原則,我們寫在哪個類里的變量應該盡量只符合該類的作用,所以PlayerController里的變量的意義在於更好的實現控制。比如假設玩家在一個關卡內可以按AABB來作弊獲得100金幣,但是限最多3次。那么這個按鍵的響應就應該由PlayerController來接收,然后調用AddCoin(100),並更新PlayerController里的成員變量CoinCheatCount。也或者想實現馬里奧的加速跑,也可以在PlayerController里增加Speed的成員變量。
- 記住PlayerController是可被替換的,不同的關卡里也可能是不一樣的。比如馬里奧在水下的時候控制的方式明顯就不一樣,所以就不能像“Player”單件類那樣什么都往里面塞。這樣一旦被替換掉了之后數據就都丟失了。
- PlayerController也不一定存在,考慮一下如果把馬里奧做成聯機游戲,那么對方玩家被同步過來的將只有PlayerState,對方玩家的PlayerController只在服務器上存在。所以這個時候,如果你把金幣數據放在PlayerController里的話就非常尷尬了。所以為了擴展性來說,還是根據職責分明的原則來正確划分業務邏輯會比較好。
- 在任一刻,Player:PlayerController:PlayerState是1:1:1的關系。但是PlayerController可以有多個備選用來切換,PlayerState也可以相應多個切換。UPlayer的概念會在之后講解,但目前可以簡單理解為游戲里一個全局的玩家邏輯實體,而PlayerController代表的就是玩家的意志,PlayerState代表的是玩家的狀態。
AAIController
從某種程度上來說,AI也可以算是一個Player,只不過它不需要接收玩家的控制,可以自行決策行動。從玩家控制的邏輯需要有一個載體一樣,AI的邏輯算法也需要有一個運行的實體。而這就是UE里的AIController:
同PlayerController對比,少了Camera、Input、UPlayer關聯,HUD顯示,Voice、Level切換接口,但也增加了一些AI需要的組件:
- Navigation,用於智能根據導航尋路,其中我們常用的MoveTo接口就是做這件事情的。而在移動的過程中,因為少了玩家控制的來轉向,所以多了一個SetFocus來控制當前的Pawn視角朝向哪個位置。
- AI組件,運行啟動行為樹,使用黑板數據,探索周圍環境,以后如果有別的AI算法方法實現成組件,也應該在本組件內組合啟動。
- Task系統,讓AI去完成一些任務,也是實現GameplayAbilities系統的一個接口。目前簡單來說GameplayAbilities是為Actor添加額外能力屬性集合的一個模塊,比如HP,MP等。其中的GamePlayEffect也是用來實現Buffer的工具。另外GamePlayTags也是用來給Actor添加標簽標記來表明狀態的一種機制。目前來說該兩個模塊似乎都是由Epic的Game Team在維護,所以完成度不是非常的高,用的時候也往往需要根據自己情況去重構調整。
本文重點不在於討論AI內部的各種組件功能,因此我們先把目光聚焦在AIController對象本身上。同PlayerController一樣,AIController也只存在於Server上(單機游戲也可看作是Server)。游戲里必須有玩家參與,而AI可以沒有,所以AIController並不一定會存在。我們可以在Pawn上配置AIControllerClass來讓該Pawn產生的時候自動為它分配一個AIController,之后自動釋放。
思考:哪些邏輯應該放在AIController中?
我們依然要思考這個問題,大部分思想和原則和PlayerController是一樣的,只不過AI算法的多種多樣,所以我們推薦盡量利用UE提供的行為樹黑板等組件實現,而不是直接在AIController硬編碼再度實現。也請把目光僅僅局限在當前的Pawn身上,不要在里面寫其他無關的邏輯。另外,因為AIController都是在關卡內比較短暫存在的,一般不太有垮Level的數據保存,所以你可以用AIController的成員變量來保存狀態。而如果真的需要用到PlayerController的狀態,則也可以引用一個PlayerState過來。如果想引用關卡的全局狀態,也可以引用GameState,再更高級別的,甚至可以直接和GameInstance接觸。
但是AIController也可以通過配置bWantsPlayerState來獲得自己的PlayerState,所以PlayerState其實也並不是跟UPlayer綁定的,畢竟從本質上來說APlayerState也只是個AInfo(AActor),跟其他Actor一樣可以有多個,並沒有什么稀奇的,區別是你自己怎么創建並利用它。
總結
到此,我們也算討論完了Actor(Pawn)層次的控制,在這個層次上,我們關注的焦點在於如何更好的控制游戲世界里各種Actor交互和邏輯。UE采用了分化Actor的思維創建出AController來控制APawn們,因為玩家玩游戲也全都是控制着游戲里的一個化身來行動,所以UE抽象總結分化了一個APlayerController來上接Player的輸入,下承Pawn的控制。對於那些自治的AI實體,UE給予了同樣的尊重,創建出AIController,包含了一些方便的AI組件來實現游戲邏輯。並利用PlayerState來存儲狀態數據,支持在網絡間同步。
上圖應該可以比較清晰的闡明,UE是如何充分利用Actor的本身機制來反過來實現對Actor的邏輯控制,相信親愛的讀者朋友們也能自行體會到它的優雅之處。對比其他的游戲引擎,往往它們都止步於Actor這一個層次,只提供了最基本的對象層次,美名其曰交給玩家控制。UE為我們提供了這一套簡潔強大的機制,大大方便了我們編寫邏輯的難度。
而下篇我們的邏輯之旅將再繼續拔高一個層次,將開始講解World層次的邏輯,這個世界的意志:GameMode!
下篇:GamePlay架構(七)GameMode和GameState
引用
UE 4.13.2
知乎專欄:InsideUE4
UE4深入學習QQ群: 456247757(非新手入門群,請先學習完官方文檔和視頻教程)
個人原創,未經授權,謝絕轉載!