網頁版
https://yekdniwue.blogspot.com/2020/08/CustomMove2.html
簡介
在前一篇我已經介紹一個完整的範例,
說明如何製作一個支援網路的功能,
如何做測試,
跟如何驗證這個做好的功能,
在網路延遲的環境是否能正常運作。
前一個做法確定是不行的,玩家會感到畫面不流暢。
因此本篇要介紹如何正確地修改移動相關的功能,
讓Server端能夠信任Client發送的指令,
避免Server頻繁的矯正Client的位置。
這篇其實會比較偏向程式碼實作,敘述會少很多。
簡單來說就是看code比較快啦。
當初是從CharacterMovementComponent的Crouch挖出來的。
所以如果想要自己試試看的話,可以去挖出引擎有關Crouch的程式來看。
跟之前一樣,裡面的程式碼都是我經過精簡過了,
多餘的實作項目我盡可能的都沒列在裡面,
所以最好還是按照順序看完,以免出錯。
再繼續往下看之前,請確認以下的詞你都知道是什麼意思:
1. Replicated Property
2. ROLE Authority
3. ROLE Simulated Proxy
4. ROLE AutonomousProxy
礙於篇幅的關係,這邊不會多做介紹。如果有不熟悉的項目,請先前往
https://docs.unrealengine.com/en-US/Gameplay/Networking/Actors/Roles/index.html
惡補一下。
為角色新增移動模式
要製作這種由玩家的操作改變移動速度的作法,
其實要用到的是MovementMode的切換。
也就是製作一個新的MovementModeStrafe,
然後玩家按鍵的時候進到這個MovementMode,
放開的時候回到預設的MovementMode。
為了達到這個目的,我們總共需要新增
兩個C++ class,以及一個BP class,
所以就是5個檔案。
1. CustomCharacter
2. CustomMoveComponent
3. BP_CustomCharacter
而移動速度的改變,核心做法是override GetMaxSpeed(),
如果角色正在Strafe狀態,MaxSpeed就回傳
Super::GetMaxSpeed()*StrafeSpeedRatio
CustomCharacter實作
大致上要實作的項目
CustomCharacter要能接受玩家的輸入,所以跟之前的作法一樣,
要開出Strafe以及UnStrafe兩個函式給外部使用。
為了得知Strafe/UnStrafe的狀態變化時機,所以我也開出了四個函式:
1. OnStartStrafe
2. OnEndStrafe
3. BP_OnStartStrafe
4. BP_OnEndStrafe
分別是C++以及BP的事件,提供給Gameplay需要的時候使用。
Server需要將Movement的狀態同步給SimulatedProxy,
所以要新增一個變數bIsStrafed,
用來讓SimulatedProxy獲得事件通知。
也因為要Replicate變數bIsStrafed,
就要實作GetLifetimeReplicatedProps函式。
因為我們會以CustomMoveComponent取代
原生的CharacterMovementComponent,
我會建立一個指標指向CustomMoveComponent。
然後在Constructor的時候做替換。
替換MoveComponent以及同步變數
在Constructer替換MoveComponent,
然後在GetLifetimeReplicatedProps同步變數bIsStrafed,
並且只同步給Simulated Proxy。
因為Autonumous Proxy在輸入的當下就切換狀態了。
Strafe與UnStrafe實作
在CustomCharacter內,Strafe跟UnStrafe只是負責
將指令帶給CustomMoveComponent,
後續就交給CustomMoveComponent在傳遞資訊的時候做處理。
變數同步與事件通知
收到變數bIsStrafed改變的通知時,
我們就是呼叫CharMovement對應的函式做處理。
而收到OnStartStrafe與OnEndStrafe時,就是呼叫BP版本的對應函式。
可能會有人有疑惑說為什麼一個事件要分C++跟BP兩個函式,
而不是直接使用BlueprintNativeEvent直接一個函式定義起來。
主要是因為如果使用NativeEvent,那BP端是可以不呼叫C++實作的。
這個做法可以確保C++的部分一定會被執行到,不會被跳過。
到這邊CustomCharacter的實作就結束了。
CustomMoveComponent實作
大致上要實作的項目
CustomCharacter的實作其實還是比較偏Gameplay層,
就是開出函式,建立事件通知。
CustomMoveComponent要實作的項目才是核心的部份,
裡面的程式碼比較少見(其他系統不會看到這些類別與函式)。
CustomMoveComponent的實作根據不同的ROLE,有不同的實作部份
1. Autonomuous Proxy handling
2. Authority handling
Autonomuous Proxy要把bWantsToStrafe的資訊塞進FCustomSavedMove中,
這樣client給Server的每個移動資訊都會帶有這個移動是否是Strafe的資訊。
Authority從FCustomSavedMove內抽出bWantsToStrafe的資訊後,
就會以正確的速度計算移動,需要減速移動就會減速移動。
因為移動跟速度變化綁定在同一包,
所以Server不會認為Client的移動有異常,就不會做位置矯正。
一些基本雜項設定
設定PawnOwner:
有兩個地方要設定PawnOwner。SetUpdatedComponent以及PostLoad。
Strafe與UnStrafe:
如果是Server(Authority)的話,直接變更bIsStrafed,
這樣SimulatedProxy會在OnRep_IsStrafed收到通知並處理。
而到底移動速度的更改怎麼實作,就是透過檢查現在是否是Strafe,
然後override GetMaxSpeed函式,如果Strafe中就乘上減速比例。
移動資訊傳遞
剩下的部份就是處理移動資訊的傳遞,
總共還有以下幾個函式在CustomMoveComponent要實作:
1. UpdateCharacterStateBeforeMovement
2. UpdateFromCompressedFlags
3. GetPredictionData_Client
修改Client送給Server的移動結構
要把Strafe的狀態告訴Server,就是要將Strafe的狀態塞進移動資料。
在FSavedMove的CompressedFlags提供了四個custom的bit可供我們使用,
也就是說我們被允許最多建立出16種移動狀態。
在這邊我會借用FLAG_Custom_0的0代表UnStrafe,1代表Starfe。
為了修改FSavedMove,我們要實作自己的版本:
class FCustomSavedMove : public FSavedMove_Character
而SavedMove是被裝在FNetworkPredictionData_Client_Character裡面。
所以我們要實作自己的版本:
class FNetworkPredictionData_Client_Custom :
public FNetworkPredictionData_Client_Character
然後實作函式AllocateNewMove,裡面回傳我們自定義的FCustomSavedMove。
最後就是在CustomMoveComponent裡面實作GetPredictionData_Client。
在GetPredictionData_Client裡面我們要回傳自定義的class
FNetworkPredictionData_Client_Custom。
這樣就能以我們自定義的移動結構取代原來的移動結構了。
將Strafe狀態整合進移動資料
可分為輸入輸出兩種情況,
輸入:Client把Strafe狀態塞進移動資料傳給Server。
輸出:Server從移動資料獲得Client送來的Strafe狀態。
輸入實作:
Client(Autonumous)傳送移動資料給Server的處理流程:
CharacterMovementComponent::TickComponent()
ReplicateMoveToServer
SetMoveFor
CanCombineWith
PerformMovement
UpdateCharacterStateBeforeMovement
CallServerMove
GetCompressedFlags
SetMoveFor:
從CustomMoveComponent複製bWantsToStrafe到FCustomSavedMove。
CanCombineWith:
如果bWantsToStrafe有變動,那兩個SavedMove要避免合併,
以免Strafe狀態的資料因為合併而遺失。
UpdateCharacterStateBeforeMovement:
在套用移動前再次檢查狀態是否有變化。
GetCompressedFlags:
將bWantsToStrafe存入CompressedFlags的FLAG_Custom_0。
輸出實作:
Server(Authority)收到玩家資料的處理流程:
ServerMove_Implementation
MoveAutonomous
UpdateFromCompressedFlags
PerformMovement
UpdateCharacterStateBeforeMovement
UpdateFromCompressedFlags:
從SavedMove的FLAG_Custom_0取得Strafe狀態,存入CustomMoveComponent。
UpdateCharacterStateBeforeMovement:
在套用移動前再次檢查狀態是否有變化。
Server端Strafe狀態的變化事件會在這裡觸發。
程式碼在哪?
說了這麼多,你一定想問"到底要不要附程式碼"
你可以參考我當初找到的範例
[連結]
或是參考UnrealEngine的原始碼有關bWantsToCrouch相關的部份。
或是參考我實作的版本,已做成Plugin,但是BP角色的串接就要自己實作了。
[連結]
最後一哩路
按照以上作法實作,BP的character改為繼承CustomCharacter之後,
只要在使用者輸入的時候呼叫Strafe/UnStrafe,就完成了。
其他的事情都已經在C++處理完畢。如圖所示。
[圖]
實作完畢後你可以用前一篇提的方法做測試,
會發現就算lag設為500ms也不會有異常。
可參考影片:
[影片]
結論
本系列所提的方法是為了解決如果玩家操作可以改變移動速度,
如果沒有把狀態變化跟移動資料一起傳給Server,
Server與Client就會計算出不同的位置,
然後Client就會收到Server傳來的位置矯正,
造成玩家感受不良的問題。
然而一般常見的玩家對玩家,例如緩速技能,凍結技能,
這種牽扯的對象不是只有玩家對Server,
就沒有辦法使用這種方式處理。
要解決這種問題的困難度也高很多。
目前沒有繼續往後研究下去,所以這個系列應該會到此結束。
哪一天真的有實作出來並且經過驗證再分享吧(應該不會有機會)。