在多人遊戲環境變更角色移動速度 (二)

簡介


在前一篇我已經介紹一個完整的範例,說明如何製作一個支援網路的功能,如何做測試,跟如何驗證這個做好的功能在網路延遲的環境是否能正常運作。

前一個做法確定是不行的,玩家會感到畫面不流暢,因此本篇要介紹如何正確地修改移動相關的功能,讓Server端能夠信任Client發送的指令,避免Server頻繁的矯正Client的位置。

這篇其實會比較偏向程式碼實作,敘述會少很多。簡單來說就是看code比較快啦。
當初是從CharacterMovementComponent的Crouch挖出來的。
所以如果想要自己試試看的話,可以去挖出引擎有關Crouch的程式來看。

跟之前一樣,裡面的程式碼都是我經過精簡過了,多餘的實作項目我盡可能的都沒列在裡面,所以最好還是按照順序看完,以免出錯。

再繼續往下看之前,請確認以下的詞你都知道是什麼意思:

1. Replicated Property
2. ROLE Authority
3. ROLE Simulated Proxy
4. ROLE AutonomousProxy

礙於篇幅的關係,這邊不會多做介紹。如果有不熟悉的項目,請先前往
惡補一下。

為角色新增移動模式


要製作這種由玩家的操作改變移動速度的作法,其實要用到的是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. Autonomous Proxy handling
2. Authority handling


Autonomous 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,就沒有辦法使用這種方式處理。要解決這種問題的困難度也高很多。

目前沒有繼續往後研究下去,所以這個系列應該會到此結束。哪一天真的有實作出來並且經過驗證再分享吧(應該不會有機會)。


留言

這個網誌中的熱門文章

UE4 除錯技巧分享 (一)

UE4 GameplayAbilitySystem - GameplayEffect & GameplayCue 如何設定參數

UE4 GameplayAbilityTask介紹