效果描述
一個服務器,兩個客戶端,讓他們連接后分別生成不同的Pawn,並且在不同的位置生成。
意義
這是個項目需求,但是我發現如果能夠徹底理解並制作出這個功能,會對虛幻4內置的網絡功能以及一些重要的Gameplay 的類有更深入的了解。
目前已有解決方案
在Google上也搜了好久,但是相關信息並不多,比較靠譜的最終都指向同一個wiki頁面:Spawn Different Pawns For Players in Multiplayer,但是他的解決方案有個缺陷:開發過程中不好測試——原因是他的方案需要一個外部存儲的數據或者SaveGame,而開發時你不可能把一個工程復制兩份。除非發布以后,把發布好的文件復制兩份去測試,但這樣也很麻煩。
我的解決方案概述
我采用了藍圖實現。區分不同的客戶端主要是依靠OnPostLogin的連接順序。
在GameMode中設置一個int變量作為用戶索引,每次OnPostLogin后遞增1。因為服務器(Listen Server)的PostLogin肯定是第一個(沒哪個客戶端的連接速度會快過本機),所以服務端連接時索引為0。而其他兩個客戶端在項目中並不需要立即區分其角色,只需要先分別給一個不同的角色,后面如果需要修改,在服務端提供界面進行手動修改即可。
預備知識
- 首先要熟悉UE4中的網絡/復制/RPC相關概念以及引擎GameMode,PlayerController等類在網絡環境中的表現,這些信息需要仔細閱讀和理解官方的文檔:
NetWorking and Multiplayer 其中的ActorReplication章節非常重要需要仔細閱讀和理解,MultiPlayer in Blueprints章節是個引導概況性質的章節,也需要仔細讀
- 其次要閱讀GameModeBase源碼,了解從PostLogin到真正生成Pawn之間都發生了什么,這里我已經總結成了一幅圖:
具體實施步驟
准備工作
先使用ThirdPerson模板創建工程,然后創建ThirdPersonCharacter的三個子類,給不同的顏色作為標記,分別是紅,綠,藍,其中紅色准備作為服務器的Pawn,其他兩個作為客戶端的。
以GameModeBase為基類創建一個GameMode藍圖,這里命名為MyGameMode
以PlayerController為基類窗機一個Controller藍圖,這里明明為MyPlayerController
創建一個Enum,命名為ClientType,包含三個值:Server, ClientA, ClientB
在ThirdPersionExampleMap中,刪掉場景中的Character,然后把PlayerStart復制2個出來,擺放好位置,分別給三個PlayerStart的Player Start Tag屬性設置為Server,ClientA,ClientB
關鍵步驟
MyPlayerController
在MyPlayerController中創建一個變量:
MyClientType:ClientType枚舉類型,設置為Replicated,用於標記該PlayerController的類型
MyGameMode
在MyGameMode中,創建6個變量:
ClientIndex : int型,默認設置,用於標記不同的客戶端連接,每次OnPostLogin后會遞增1
PlayerStarts:PlayerStart引用類型,數組,其他默認,用於存放所有的PlayerStart
ServerPawnClass: Pawn類類型,默認設置,表示服務器端Pawn的類
ClientAPawnClass: Pawn類類型,默認設置,表示客戶端APawn的類
ClientBPawnClass: Pawn類類型,默認設置,表示客戶端BPawn的類
CurrentPlayer: MyPlayerController引用類型,默認設置,臨時存放傳入的PlayerController
創建兩個關於獲取PlayerStart的函數:
GetAllPlayerStarts
GetPlayerStartByTag:
這兩個函數的含義如其名稱,功能也比較簡單。不過需要注意調用時機。有人可能會想,直接在BeginPlay里調用GetAllPlayerStarts就可以了,實際上這樣不行,因為OnPostLogin事件會在BeginPlay之前發生。
右鍵搜索OnPostLogin, 創建Event OnPostLogin事件,連接如下圖:
步驟釋義:
- 當有玩家連接進來后(包括服務器自身連接自身),把PlayerController存入一個臨時變量Current Player。
- 根據Client Index設置Current Player的MyClientType,依次設置為Server,ClientA,ClientB。
- 然后把Client Index自增1。
- 判斷Controller是否已經擁有了Pawn,如果有則銷毀。
- 調用Restart Player重新生成該Controller的Pawn(注意看上文中的流程圖,Restart Player之后進行了什么操作)
到這步之后,Restart之后並沒有改變要使用的Pawn的類。
根據上文中的流程圖,Pawn的類是在GetDefaultPawnClassForController函數中獲取的,在三處都使用了該函數來返回Pawn的類型,因此我們需要覆蓋這個函數,點"Functions"中的Override按鈕,覆蓋該函數。
函數截圖如下:
步驟釋義:
獲取PlayerController,轉換為MyPlayerController,根據剛才存入的MyClientType來返回不同的Pawn類型。使用MyGameMode里的三個Pawn Class 變量。
到這里,Pawn類別已經可以正常區分了,但是起始點還不行,都是在同一個位置生成。下面要解決的就是區分PlayerStart。
看上文中的流程圖,可以看到,在Restart Player函數中是通過調用Find Player Start函數來決定使用哪個PlayerStart。因此要覆蓋FindPlayerStart函數。
在MyGameMode里的Functions里點"Override按鈕,覆蓋FindPlayerStart函數,覆蓋后的截圖如下:
因為之前已經給不同的PlayerController分配了不同的角色,所以這步比較簡單,也是區別My Client Type,返回不同的PlayerStart即可。
到此為止就完成了