《Exploring in UE4》遊戲角色的移動原理(下)

遊資網發表於2019-09-12
上一篇主要從單機角色的移動原理進行分析。今天這篇文章會詳細的分析多個玩家的移動同步是如何處理的。文章的內容可能比較深,需要讀者有一定遊戲開發經驗,而且結合引擎原始碼才能更好的理解,建議收藏找時間慢慢閱讀。

知乎原文連結:https://zhuanlan.zhihu.com/p/34257208

下篇

四.移動同步解決方案

前面關於移動邏輯的細節處理都是在PerformMovement裡面實現的,我們可以把函式PerformMovement當成一個完整的移動處理流程。這個流程無論是在客戶端還是在伺服器都必須要執行,或者作為一個單機遊戲,這一個介面基本上可以滿足我們的正常移動了。不過,在網路遊戲中,為了讓所有的玩家體驗一個幾乎相同的世界,需要保證一個具有絕對權威的伺服器,這個伺服器可以修正客戶端的不正常移動行為,保證各個客戶端的一致性。相關同步的操作都是基於UCharacterMovement元件實現的,所以我們的角色必須要使用這個移動元件。

《Exploring in UE4》遊戲角色的移動原理(下)

移動元件的同步全都是基於RPC不可靠傳輸的,你會在UCharacterMovement標頭檔案裡面看到多個以Server或者Client開頭的RPC函式。

關於移動元件的同步思路,建議選閱讀一下官方文件的內容,回頭看可能更為清晰一點。現在我們把整個移動細節作為一個介面封裝起來,巨集觀的研究移動元件的同步細節。

另外,如果還沒有完全搞清Authority,AutonomousProxy以及SimulatedProxy的概念,請參考我的知乎文章“UE4網路同步詳解(一)——理解同步規則”。這裡舉個例子,一個伺服器上有一個玩家ServerA和一個NPC ServerB,客戶端上擁有從伺服器複製過來的這個玩家ClientA與NPC ClientB。由於ServerA與ServerB都是在伺服器上生成的,所以他們兩在伺服器上的所有權Role都是ROLE_Authority。ClientA在客戶端上由於被玩家控制,他的Role是ROLE_AutonomousProxy。ClientB在客戶端是完全通過伺服器同步來控制的,他的Role就是ROLE_SimulatedProxy。

《Exploring in UE4》遊戲角色的移動原理(下)

4.1伺服器角色正常的移動流程

第三章節裡面的圖3-1就是單機或者ListenServer伺服器執行的移動流程。作為一個本地控制的角色,他只需要認真的執行正常的移動(PerformMovement)邏輯處理即可,所以ListenServer伺服器移動不再贅述。

但是對於DedicateServer,他的本地沒有控制的角色,對移動的處理就有差異了。分為兩種情況:

  • 該角色在客戶端是模擬(Simulate)角色,移動完全由伺服器同步過去,如各類AI角色。這類移動一般是伺服器上行為樹主動觸發的。
  • 該角色在客戶端是擁有自治(Autonomous)權利的Character,如玩家控制的主角。這類移動一般是客戶端接收玩家輸入資料本地模擬後,再通過RPC發給伺服器進行模擬的。


從下面的程式碼可以瞭解到這兩種情況的處理(注意註釋):

  1. // UCharacterMovementComponent:: TickComponent
  2. // simulate的角色在伺服器執行IsLocallyControlled也會返回true
  3. // Allow root motion to move characters that have no controller.
  4. if( CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()) )
  5. {
  6.    {
  7.        SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);

  8.        // We need to check the jump state before adjusting input acceleration, to minimize latency
  9.        // and to make sure acceleration respects our potentially new falling state.
  10.        CharacterOwner->CheckJumpInput(DeltaTime);

  11.        // apply input to acceleration
  12.        Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));
  13.        AnalogInputModifier = ComputeAnalogInputModifier();
  14.    }

  15.    if (CharacterOwner->Role == ROLE_Authority)
  16.    {
  17.        // 單機或者DedicateServer控制simulate角色移動
  18.        PerformMovement(DeltaTime);
  19.    }
  20.    else if (bIsClient)
  21.    {
  22.        ReplicateMoveToServer(DeltaTime, Acceleration);
  23.    }
  24. }
  25. else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)
  26. {
  27.    //DedicateServer控制自治客戶端角色移動
  28.    // Server ticking for remote client.
  29.    // Between net updates from the client we need to update position if based on another object,
  30.    // otherwise the object will move on intermediate frames and we won't follow it.
  31.    MaybeUpdateBasedMovement(DeltaTime);
  32.    MaybeSaveBaseLocation();

  33.    // Smooth on listen server for local view of remote clients. We may receive updates at a rate different than our own tick rate.
  34.    if (CharacterMovementCVars::NetEnableListenServerSmoothing && !bNetworkSmoothingComplete && IsNetMode(NM_ListenServer))
  35.    {
  36.        SmoothClientPosition(DeltaTime);
  37.    }
  38. }
複製程式碼

這兩種情況詳細的流程我們在下面兩個小結分析。

4.2 Autonomous角色

一個客戶端的角色是完全通過伺服器同步過來的,他身上的移動元件也一樣是被同步過來的,所以遊戲一開始客戶端的角色與伺服器的資料是完全相同的。對於Autonomous角色,大致的實現思路如下:

客戶端通過接收玩家的Input輸入,開始進行本地的移動模擬流程,移動前首先建立一個移動預測資料結構FNetworkPredictionData_Client _Character,執行PerformMovement移動,隨後儲存當前的移動資料(速度,旋轉,時間戳以及移動結束後的位置等資訊)到前面的FNetworkPredictionData裡面的SavedMoves列表裡面,並通過RPC將當前的Move資料傳送該資料到伺服器。然後繼續進行TickComponent操作,重複這個流程。

客戶端在傳送給伺服器RPC訊息的同時,本地還會不斷的執行移動模擬。SavedMoves列表裡面的資料也就越來越多。如果這時候收到了一個ClientAckGoodMove呼叫,那麼表示伺服器接收了對應時間戳的客戶端移動,客戶端就將這個時間戳之前的SavedMoves全部移除。如果客戶端收到了ClientAdjustPosition呼叫,那麼表示對應這個時間戳的移動有問題,客戶端需要修改成伺服器傳過來的位置,並重新播放那些還沒被確認的SaveMoves列表裡面的移動。

《Exploring in UE4》遊戲角色的移動原理(下)
圖4-1

整個流程如下圖所示:

《Exploring in UE4》遊戲角色的移動原理(下)
圖4-2 Autonomous角色移動流程圖

4.2.1 SavedMoves與移動合併

仔細閱讀原始碼的朋友對上面給出的流程可能並不是很滿意,因為除了ServerMove你可能還看到了ServerMoveDual以及ServerMoveOld等函式介面。而且除了SavedMoves列表,還有PendingMove、FreeMove這些移動列表。他們都是做什麼的?

簡單來講,這屬於移動頻寬優化的一個方式,將沒有意義的移動合併,減少訊息的傳送量。

當客戶端執行完本次移動後,都會把當前的移動資料以一個結構體儲存到SavedMove列表,然後會判斷當前的這個移動是否可以被延遲傳送(CanDelaySendingMove(),預設為true),如果可以就會繼續判斷當前的客戶端網路速度如何。如果當前的速度有一點慢或者上次更新的時間很短,移動元件就會將當前的移動賦值給PendingMove(表示將要執行的移動)並取消本次給伺服器訊息的傳送。

  1. const bool bCanDelayMove = (CharacterMovementCVars::NetEnableMoveCombining != 0) && CanDelaySendingMove(NewMove);

  2. if (bCanDelayMove && ClientData->PendingMove.IsValid() == false)
  3. {
  4.    // Decide whether to hold off on move
  5.    // send moves more frequently in small games where server isn't likely to be saturated
  6.    float NetMoveDelta;
  7.    UPlayer* Player = (PC ? PC->Player : nullptr);
  8.    AGameStateBase const* const GameState = GetWorld()->GetGameState();

  9.    if (Player && (Player->CurrentNetSpeed > 10000) && (GameState != nullptr) && (GameState->PlayerArray.Num() <= 10))
  10.    {
  11.        NetMoveDelta = 0.011f;
  12.    }
  13.    else if (Player && CharacterOwner->GetWorldSettings()->GameNetworkManagerClass)
  14.    {
  15.        //這裡會根據網路管理的配置以及客戶端網路速度來決定是否延遲傳送
  16.        NetMoveDelta = FMath::Max(0.0222f,2 * GetDefault<AGameNetworkManager>(CharacterOwner->GetWorldSettings()->GameNetworkManagerClass)->MoveRepSize/Player->CurrentNetSpeed);
  17.    }
  18.    else
  19.    {
  20.        NetMoveDelta = 0.011f;
  21.    }

  22.    if ((GetWorld()->TimeSeconds - ClientData->ClientUpdateTime) * CharacterOwner->GetWorldSettings()->GetEffectiveTimeDilation() < NetMoveDelta)
  23.    {
  24.        // Delay sending this move.
  25.        ClientData->PendingMove = NewMove;
  26.        return;
  27.    }
  28. }
複製程式碼


當客戶端進去下次Tick的時候,就會判斷當前的新的移動是否能與上次儲存的PendingMove合併。如果可以,就可以減少一次訊息的傳送。如果不能合併,那麼在本次移動結束後給伺服器傳送一個兩次移動(ServerMoveDual),就是單純的執行兩次ServerMove。

伺服器在受到兩次移動的時候對第一次移動不進行任何校驗,只對第二個移動進行正常的校驗,判斷是否是第一次的標準就是ClientPosition是不是FVector(1.f,2.f,3.f)。通過下面的程式碼就可以瞭解了

  1. void UCharacterMovementComponent::ServerMoveDual_Implementation(
  2.    float TimeStamp0,
  3.    FVector_NetQuantize10 InAccel0,
  4.    uint8 PendingFlags,
  5.    uint32 View0,
  6.    float TimeStamp,
  7.    FVector_NetQuantize10 InAccel,
  8.    FVector_NetQuantize100 ClientLoc,
  9.    uint8 NewFlags,
  10.    uint8 ClientRoll,
  11.    uint32 View,
  12.    UPrimitiveComponent* ClientMovementBase,
  13.    FName ClientBaseBone,
  14.    uint8 ClientMovementMode)
  15. {
  16.    ServerMove_Implementation(TimeStamp0, InAccel0, FVector(1.f,2.f,3.f), PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBone, ClientMovementMode);
  17.    ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBone, ClientMovementMode);
  18. }
複製程式碼


其實,UE的思想就是,將所有的移動的關鍵資訊都資料化,這樣移動就可以自由的儲存和回放。為了節省頻寬,提高效率,我們也就可以想出各種辦法來減少傳送不必要的訊息,對於一個沒有移動過的玩家,理論上我們甚至都可以不去同步他的移動資訊。

《Exploring in UE4》遊戲角色的移動原理(下)
圖4-3移動預測及儲存的資料結構示意圖

4.3 Simulated角色

首先看一下官方文件對Simulate角色移動的描述:

對於那些不由人類控制的人物,其動作往往會通過正常的PerformMovement()程式碼在伺服器(此時充當了主控者)上進行更新。Actor的狀態,如方位、旋轉、速率和其他一些選定的人物特有狀態(如跳躍)都會通過正常的複製機制複製到其他機器,因此,它們不必在每一幀都經由網路傳送。為了在遠端客戶端上針對這些人物提供更流暢的視覺呈現,該客戶端機器將在每一幀為模擬代理執行一次模擬更新,直到新的資料(由伺服器主控)到來。本地客戶端檢視其他遠端人類玩家時也是如此;遠端玩家將其更新傳送給伺服器,後者為該玩家執行一次完整的動作更新,然後定期複製資料給所有其他玩家。這個更新的作用是根據複製的狀態來模擬預期的動作結果,以便在下一次更新前“填補空缺”。所以,客戶端並沒有在新的位置放置由伺服器傳送的代理,然後將它們保留到下次更新到來(可能是幾個後續幀),而是通過應用速率和移動規則,在每一幀模擬出一次更新。在另一次更新到來時,客戶端將重置本地模擬並開始新一次模擬。

簡單來說,Simulate角色的在伺服器上的移動就是正常的PerformMovement流程。而在客戶端上,該角色的移動分成兩個步驟來處理——收到伺服器的同步資料時就直接進行設定。在沒有收到伺服器訊息的時候根據上一次伺服器傳過來的資料(包括速度與旋轉等)在本地執行Simulate模擬,等著下一個同步資料到來。Simulate角色採用這樣的機制,本質上是為了減小同步帶來的開銷。下面程式碼展示了所有Character的同步屬性


  1. void ACharacter::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
  2.    {
  3.        Super::GetLifetimeReplicatedProps( OutLifetimeProps );
  4.        DOREPLIFETIME_CONDITION( ACharacter, RepRootMotion,COND_SimulatedOnlyNoReplay);
  5.        DOREPLIFETIME_CONDITION( ACharacter, ReplicatedBasedMovement,   COND_SimulatedOnly );
  6.        DOREPLIFETIME_CONDITION( ACharacter, ReplicatedServerLastTransformUpdateTimeStamp, COND_SimulatedOnlyNoReplay);

  7.        DOREPLIFETIME_CONDITION( ACharacter, ReplicatedMovementMode,    COND_SimulatedOnly );
  8.        DOREPLIFETIME_CONDITION( ACharacter, bIsCrouched,           COND_SimulatedOnly );

  9.        // Change the condition of the replicated movement property to not replicate in replays since we handle this specifically via saving this out in external replay data
  10.        DOREPLIFETIME_CHANGE_CONDITION(AActor,ReplicatedMovement,COND_SimulatedOrPhysicsNoReplay);
  11.    }
複製程式碼

ReplicatedMovement記錄了當前Character的位置旋轉,速度等重要的移動資料,這個成員(包括其他屬性)在Simulate或者開啟物理模擬的客戶端才執行(可以先忽略NoReplay,這個和回放功能有關)。同時,我們可以看到Character大部分的同步屬性都是與移動同步有關,而且基本都是SimulatedOnly,這表示這些屬性只在模擬客戶端才會進行同步。除了ReplicatedMovement屬性以外,Replicated MovementMode同步了當前的移動模式,ReplicatedBasedMovement同步了角色所站在的Component的相關資料,Replicated-ServerLastTransform-Update TimeStamp同步了最新的伺服器移動更新幀,也就相當於最後一次伺服器更新移動的時間(在ACharacter:reReplication裡會將伺服器當前的移動資料賦值給Replicated-ServerLastTransform-UpdateTimeStamp然後進行同步)。

瞭解了這些同步的資料後,我們開始分析其移動流程。流程如下圖所示(RootMotion的情況我在上一章節已經描述,這裡不再贅述)。其實其基本思路與普通的移動處理相似,只不過是呼叫SimulateTick去根據當前的速度等條件模擬客戶端移動,但是有一點非常重要的差異就是Simulate的角色的膠囊體移動與Mesh移動是分開進行的。這麼做的原因是什麼呢?我們稍後再解釋。

《Exploring in UE4》遊戲角色的移動原理(下)
圖4-4 Simulate角色移動流程圖

客戶端的模擬我們大致瞭解了流程,那麼接收伺服器資料並修正是在哪裡處理的呢?答案是AActor::OnRep_ReplicatedMovement。

Simulateticked客戶端是完全走屬性同步的,客戶端在接收到伺服器同步的ReplicatedMovement時,會產生回撥函式觸發SmoothCorrection的執行,從當前客戶端的位置平滑的過度到伺服器同步的位置,同時會通過APawn::PostNetReceiveVelocity修改當前的移動速度,隨後的客戶端在Simulate時就可以用這個速度進行模擬。SmoothCorrection是插值的核心函式之一。(參考ACharacter的PostNetReceiveLocation AndRotation)

那麼這裡描述一下:SmoothCorrection到底做了什麼?

  • Replay會直接返回,Disabled Mode會直接設定膠囊體位置
  • GetPredictionData_Client_Character客戶端獲取預測資料結構體,第一次會主動建立。這裡面會儲存一段時間內的移動資料以及時間戳,還有各種位置和旋轉的偏移用於平滑
  • 記錄一箇舊座標到新座標的偏移NewToOldVector,同時算出一個二維的距離。如果偏移距離太大,可能不去設定偏移。如果小於MaxSmoothNetUpdateDist,就設定MeshTranslationOffset為當前的值加上NewToOldVector
  • 如果是線性插值,先將當前的OriginalMeshTranslationOffset設定為前面剛計算的MeshTranslationOffset;設定OriginalMeshRotationOffset以及MeshRotationOffset都為OldRotation,設定MeshRotationTarget為NewRotation,設定膠囊體到新的座標(不修改其rotation,也不修改Mesh的位置)
  • 如果是指數插值,記錄相對新的Rotation的MeshRotationOffset,直接同時設定膠囊體的location以及Rotation
  • 如果當前的客戶端smooth時間戳大於伺服器的,就把當前的時間戳lerp到伺服器。
  • 獲取伺服器同步的ReplicatedServerLastTransformUpdateTimeStamp,然後賦值給SmoothingServerTimeStamp
  • 計算伺服器兩次傳遞資料的時間差,根據預設的配置得到一個MaxDelta
  • 設定SmoothingClientTimeStamp,範圍在SmoothingServerTimeStamp-MaxDelta與SmoothingServerTimeStamp之間
  • 記錄客戶端與伺服器的時間差,LastCorrectionDelta,其實可以認為伺服器領先客戶端的時間。同時還有一個被伺服器的糾正時間LastCorrectionTime,也就是當前客戶端的時間


所以這裡我們就明白了前面提到的膠囊體與Mesh的移動分開處理,其目的就是提高代理模擬的流暢度。而且在官方文件上有簡單的例子,

比如這種情況,一個replicated的狀態顯示當前的角色在時間為t=0的時刻以速度(100,0,0)移動,那麼當時間更新到t=1的時候,這個模擬的代理將會在X方向移動100個單位,然後如果這時候服務端的角色在傳送了那個(100,0,0)的replcated資訊後立刻不動了,那麼這個replcated資訊則會使到服務端角色的位置和客戶端的模擬位置處於不同的點上。

為了避免這種“突變”情況,UE採用了Mesh網格的平滑操作。膠囊體的移動正常進行,但是其對應的Mesh網格不隨膠囊體移動,而要通過simulateTick的SmoothClientPosition處理。在SmoothNetUpdateTime時間內,移動元件會通過ClientData->MeshTranslationOffset去差值平滑Mesh相對膠囊體的偏移而並不會修改膠囊體的座標,這樣在通常情況下玩家在視覺上就不會覺得代理角色的位置突變。通過FScopedPreventAttached ComponentMove類可以限制某個元件暫時不跟隨父類元件移動。

另外,有一點需要注意,目前模擬客戶端的移動更新有幾種模式,線性、指數以及replay回放,他們的差值方式不同,對角色的移動處理也有所不同。比如,線性模式就會做外插值(也就是在沒有伺服器資料的時候也會進行擴充套件插值),replay屬於回放系統的處理方式,則會讀取所有回放資料進行線性插值,但是沒有外差值。

這裡我們以常見的線性插值來做分析:

  • SmoothClientPosition是simulated客戶端Tick執行的,裡面包括兩個重要的函式一個是用於插值計算的SmoothClientPosition_Interpolate函式,另一個是應用插值更新Mesh的SmoothClientPosition_UpdateVisuals函式
  • 在SmoothClientPosition_Interpolate裡面,會用到前面的ClientData裡面記錄了伺服器領先時間LastCorrectionDelta,客戶端上次處理時間戳SmoothingClientTimeStamp,收到的伺服器時間戳SmoothingServer TimeStamp
  • SmoothClientPosition_Interpolate執行Tick來更新SmoothingClientTime Stamp(+=DeltaSeconds),由於支援外插值,所以客戶端執行0.15比例的外插。當客戶端的時間戳大於伺服器的話,就會最大外插0.15比例的offset
  • 假如我們還是客戶端落後於伺服器,那麼就會按照DeltaSeconds/LastCorrectionDelta的比例計算插值
  • 如果插值比例接近1或者大於1,就要判斷一下當前的速度是否為0,為0的話就直接設定offset為0,反之就繼續插值。如果比例遠小於1,就正常按照比例插值。
  • 由於前面一直是把資料記錄在ClientData裡面而沒有處理,所以在SmoothClientPosition_Interpolate結束後需要呼叫SmoothClientPositio n_UpdateVisuals真正的應用這些資料。
  • 根據當前的MeshRotationOffset以及MeshTranslationOffset反向計算一個Mesh的新的相對位置,用於正確的處理位置偏移以及rotation的偏移
  • 如果Rotation幾乎沒有變化,那麼直接設定相對位置即可。如果Rotation變化比較大,那麼就需要先設定前面計算的相對位置,再去設定新的rotation(這時候Mesh的Rotation會隨著膠囊體一起旋轉)


最後再通過下面的圖理解一下就非常清晰了

《Exploring in UE4》遊戲角色的移動原理(下)
圖4-5

Smooth平滑方式如下,預設我們採用Exponential:

  1. /** Smoothing approach used by network interpolation for Characters. */
  2.    UENUM(BlueprintType)

  3.     enum class ENetworkSmoothingMode : uint8
  4.     {
  5.       /** No smoothing, only change position as network position updates are received. */
  6.       Disabled     UMETA(DisplayName="Disabled"),

  7.       /** Linear interpolation from source to target. */
  8.       Linear           UMETA(DisplayName="Linear"),

  9.       /** Exponential. Faster as you are further from target. */
  10.       Exponential      UMETA(DisplayName="Exponential"),

  11.       /** Special linear interpolation designed specifically for replays. Not intended as a selectable mode in-editor. */
  12.       Replay           UMETA(Hidden, DisplayName="Replay"),
  13.     };
複製程式碼


4.4關於物理託管後的移動

一般情況下我們是通過移動元件來控制角色的移動,不過如果給玩家角色的膠囊體(一般Mesh也是)勾選了SimulatePhysics,那麼角色就會進入物理託管而不受移動元件影響,元件的同步自然也是無效了,常見的應用就是玩家結合布娃娃系統,角色死亡後表現比較自然的摔倒效果。相關程式碼如下:

  1. UCharacterMovementComponent::TickComponent
  2. // We don't update if simulating physics (eg ragdolls).
  3. if (bIsSimulatingPhysics)
  4. {
  5.    // Update camera to ensure client gets updates even when physics move him far away from point where simulation started
  6.    if (CharacterOwner->Role == ROLE_AutonomousProxy && IsNetMode(NM_Client))
  7.    {
  8.        APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
  9.        APlayerCameraManager* PlayerCameraManager = (PC ? PC->PlayerCameraManager : NULL);
  10.        if (PlayerCameraManager != NULL && PlayerCameraManager->bUseClientSideCameraUpdates)
  11.        {
  12.            PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;
  13.        }
  14.    }
  15.    return;
  16. }
複製程式碼


對於開啟物理的Character,Simulate的客戶端也是採取移動資料靠伺服器同步的機制,只不過移動的資料不是伺服器PerformMovement算出來的,而是從根元件的物理物件BodyInstance獲取的,程式碼如下,

  1. void AActor::GatherCurrentMovement()
  2. {
  3.    AttachmentReplication.AttachParent = nullptr;

  4.    UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(GetRootComponent());
  5.    if (RootPrimComp && RootPrimComp->IsSimulatingPhysics())
  6.    {
  7.        FRigidBodyState RBState;
  8.        RootPrimComp->GetRigidBodyState(RBState);

  9.        ReplicatedMovement.FillFrom(RBState, this);
  10.        ReplicatedMovement.bRepPhysics = true;
  11.    }
  12. }
複製程式碼

五.特殊移動模式的實現思路

這一章節不是詳細的實現教程,只是給大家提供常見遊戲玩法的一些設計思路,如果有時間的話也會考慮做一些實現案例。如果大家有什麼特別的需求,歡迎提出來,可以和大家一起商討合理的解決方案。

5.1二段跳,多段跳的實現

其實4.14以後的版本里面已經內建了多段跳的功能,找到Character屬性JumpMaxCount,就可以自由設定了。當然這個實現的效果有點簡陋,只要玩家處於Falling狀態就可以進行下一次跳躍。實際上常見的多段跳都是在上升的階段才可以執行的,那我們可以在程式碼里加一個條件判斷當前的速度方向是不是Z軸正方向,還可以對每段跳躍的速度做不同的修改。具體如何修改,前面3.2.1小結已經很詳細的描述了跳躍的處理流程,大家理解了就能比較容易的實現了。

《Exploring in UE4》遊戲角色的移動原理(下)

5.2噴氣式揹包的實現

噴氣式揹包表現上來說就是玩家可以藉助揹包實現一個超高的跳躍,然後可以緩慢的下落,甚至是飛起來,這幾個狀態是受玩家操作影響的。如果玩家不操作揹包,那肯定就是自然下落了。

首先我們分析一下,現有的移動狀態裡有沒有適合的。比如說Fly,如果玩家進入飛行狀態,那麼角色就不會受到重力的影響,假如我在使用噴氣揹包時進入Flying狀態,在不使用的時候切換到Falling狀態,這兩種情況好像可以達到效果。不過,如果玩家處於下落中,然後緩慢下落或者幾乎不下落的時候,玩家應該處於Flying還是Falling?這時候突然切換狀態是不是會很僵硬?

所以,最好整個過程是一個狀態,處理上也會更方便一些。那我們試試Falling如何?前面的講解裡描述了Falling的整個過程,其實就是根據重力不斷的去計算Z方向的速度並修改玩家位置(NewFallVelocity函式)。重寫給出一個介面MyNewFallVelocity來覆蓋NewFallVelocity的計算,用一個開關控制是否使用我們的介面。這樣,現在我們只需要根據上層邏輯來計算出一個合理的速度即可。可以根據玩家的輸入操作(類似按鍵時間燃料值單位燃料能量)去計算噴氣揹包的推動力,然後將這個推動力與重力相加,再應用到MyNewFallVelocity的計算中,基本上就可以達到效果了。

當然,真正做起來其實還會複雜很多。如果是網路遊戲,你要考慮到移動的同步,在客戶端角色是Simulate的情況下,你需要在SimulateTick裡面也處理NewFallVelocity的計算。再者,可能還要考慮玩家在水裡應該怎麼處理。

5.3爬牆的實現

爬牆這個玩法在遊戲裡可以說是相當常見了。刺客信條,虐殺原形,各類武俠輕功甚至很多2D遊戲裡面也有類似的玩法。

在UE裡面,由於爬牆也是一個脫離重力的表現,而且離開牆面玩家就應該進入下落狀態,所以我們可以考慮藉助Flying來實現。基本思路就是:

建立一個新的移動模式爬牆模式

在角色執行地面移動(MoveAlongFloor)的時候,一旦遇到前面的障礙,就判斷當前是否能進入爬牆狀態

檢測條件可以有,障礙的大小,傾斜度甚至是Actor型別等等。

如果滿足條件,角色就進入爬牆狀態,然後根據自己的規則計算加速度與速度,其他邏輯仿照Flying處理

修改角色動畫,讓玩家看起來角色是在爬牆(這一部分涉及動畫系統,內容比較多)

這樣基本上可以實現我們想要的效果。不過有一個小問題就是,玩家的膠囊體方向實際還是豎直方向的,因此碰撞與動畫表現可能有一點點差異。如果想表現的更好,也可以對整個角色進行旋轉。

5.4爬梯子的實現

梯子是豎直方向的,所以玩家只能在Z軸方向產生速度與移動,那麼我們直接使用Walking狀態來模擬是否可以呢?很可惜,如果不加修改的話,Walking裡面預設只有水平方向的移動,只有遇到斜面的時候才會根據斜面角度產生Z軸方向的速度。那我這裡給出一個建議,還是使用Flying。(Flying好像很萬能)

玩家在開始爬一個梯子的時候,首先要把角色的Attach到梯子上面,同時播放響應的動畫來配合。一旦玩家爬上了梯子,就應該進入了特殊的爬梯子狀態。這個狀態仔細想想,其實和前面的爬牆基本上相似,不同的就是爬梯子的速度,而且玩家可以隨時停止。

《Exploring in UE4》遊戲角色的移動原理(下)

隨時停止怎麼做?兩個思路:

參考Walking移動的計算,計算速度CalcVelocity的時候使用自定義的摩擦係數Friction以及剎車速度(這兩個值都設定大一些)

當玩家輸入結束後,也就是Accceleration=0的時候,直接設定速度為0,不執行CalcVelocity。

另外,要想讓爬梯子表現的進一步好一些。看起來是一格一格的爬,就需要特殊的控制。玩家每次按下按鈕的時候,角色必須完整的執行一定位移的移動(一定位移大小就是每個梯子格的長度)。這裡可以考慮使用根骨骼位移RootMotion,畢竟動畫驅動下比較容易控制位移,不過根骨骼位移在網路條件差的情況下表現很糟。

還有一個可以進一步優化的操作,就是使玩家的手一直貼著梯子。這個需要用IK去處理,UE商城裡面有一個案例可以參考一下。

下篇文章會為大家分析一下Rootmotion在UE4中的實現原理,盡情期待~

End

相關閱讀:
《Exploring in UE4》遊戲角色的移動原理(上)
《Exploring in UE4》遊戲角色的移動原理(下)

作者:Jerish  
來源:遊戲開發那些事
原地址:https://mp.weixin.qq.com/s/s7caRnoIMCwOPq_5G8u13Q

相關文章