JustMakeGame 4. 虚幻移动组件网络同步的一个细节处理

一个同步问题

image.png

问题的起因是我在做了这个调试UI后,某些情况下界面会抖。这个界面是我锁在角色脑袋上,所以应该是角色在抖。不开UI是看不出来的。应该是抖的很轻微。

后来我在这两篇文章找到了问题原因
在多人游戏中更新角色移动速度2
在多人游戏中更新角色移动速度1

一句话说明问题的原因:
前文我提到了我这样修改角色移动速度在网络环境下是有问题的

void ACwlCharacter::UpdateDynamicMovementSettings(ECwlGait AllowedGait)
{
    const FCwlMovementSettings MovementSettings = GetTargetMovementSettings();
    switch (AllowedGait)
    {
    case ECwlGait::Walking:
        GetCharacterMovement()->MaxWalkSpeed = MovementSettings.WalkSpeed;
        break;
    case ECwlGait::Running:
        GetCharacterMovement()->MaxWalkSpeed = MovementSettings.RunSpeed;
        break;
    case ECwlGait::Sprinting:
        GetCharacterMovement()->MaxWalkSpeed = MovementSettings.SprintSpeed;
        break;
    default:
        break;
    }

    FVector MovementCurve = MovementSettings.MovementCurve->GetVectorValue(MyCharacterMovementComponent->GetMappingSpeed());
    GetCharacterMovement()->MaxAcceleration = MovementCurve.X;
    GetCharacterMovement()->BrakingDecelerationWalking = MovementCurve.Y;
    GetCharacterMovement()->BrakingFriction = MovementCurve.Z;
}

在UE DS模式下,客户端和服务端都会同时执行这个函数,这个函数在直接修改速度值。而客户端服务端由于延迟,执行时机不一样,就会导致客户端服务端速度不一样,进而导致位置不一样,位置被服务端校验后拉回原来的位置,导致的抖动。
不怎么容易发现是因为我都是自己一个人在Play As Client模式下,在编辑器里面开发,延迟低,表现不明显。

运动组件同步流程

在解释解决方案前,我认为必须明白整个同步流程,不然直接看解决方案该继承哪个类,重写那个接口一脸懵逼。

客户端先行,同步给服务端,误差回滚

一般我们用虚幻让角色动起来不需要明白那么多。只需要在Character类中调用 AddMovementInput ,从按键输入转化一个哪个方向多少的输入就好了。
我们看这个函数

void APawn::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)
{
    UPawnMovementComponent* MovementComponent = GetMovementComponent();
    if (MovementComponent)
    {
        MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);
    }
    else
    {
        Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
    }
}

它最后会累加到角色身上的一个向量上

void APawn::Internal_AddMovementInput(FVector WorldAccel, bool bForce /*=false*/)
{
    if (bForce || !IsMoveInputIgnored())
    {
        ControlInputVector += WorldAccel;
    }
}

在CharacterMovement的Tick中是这样写的

void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
    // ...

    if (CharacterOwner->GetLocalRole() > ROLE_SimulatedProxy)
    {
        SCOPE_CYCLE_COUNTER(STAT_CharacterMovementNonSimulated);

        // If we are a client we might have received an update from the server.
        const bool bIsClient = (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client));
        if (bIsClient)
        {
            FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
            if (ClientData && ClientData->bUpdatePosition)
            {
                ClientUpdatePositionAfterServerUpdate();
            }
        }

        // Allow root motion to move characters that have no controller.
        if (CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()))
        {
            ControlledCharacterMove(InputVector, DeltaTime);
        }
        else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)
        {
            // Server ticking for remote client.
            // Between net updates from the client we need to update position if based on another object,
            // otherwise the object will move on intermediate frames and we won't follow it.
            MaybeUpdateBasedMovement(DeltaTime);
            MaybeSaveBaseLocation();

            // Smooth on listen server for local view of remote clients. We may receive updates at a rate different than our own tick rate.
            if (CharacterMovementCVars::NetEnableListenServerSmoothing && !bNetworkSmoothingComplete && IsNetMode(NM_ListenServer))
            {
                SmoothClientPosition(DeltaTime);
            }
        }
    }
    else if (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy)
    {
        if (bShrinkProxyCapsule)
        {
            AdjustProxyCapsuleSize();
        }
        SimulatedTick(DeltaTime);
    }

    // ...

}

我们从这里开始看

    if (CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()))
    {
        ControlledCharacterMove(InputVector, DeltaTime);
    }

这个函数,如果是服务端就PerformMovement执行这个移动,如果是主客户端,就ReplicateMoveToServer,同步给服务端

void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds)
{
    // ...

    if (CharacterOwner->GetLocalRole() == ROLE_Authority)
    {
        PerformMovement(DeltaSeconds);
    }
    else if (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client))
    {
        ReplicateMoveToServer(DeltaSeconds, Acceleration);
    }
}

我们看ReplicateMoveToServer,代码很长,不展开了。找里面这一行。
这个函数会先,本地先执行移动操作

PerformMovement(NewMove->DeltaTime);

然后如果需要发给服务端

// Send move to server if this character is replicating movement
if (bSendServerMove)
{
    SCOPE_CYCLE_COUNTER(STAT_CharacterMovementCallServerMove);
    if (ShouldUsePackedMovementRPCs())
    {
        CallServerMovePacked(NewMove, ClientData->PendingMove.Get(), OldMove.Get());
    }
    else
    {
        CallServerMove(NewMove, OldMove.Get());
    }
}

PendingMove 也在里面。理解为客户端先行,然后发往服务器。服务器会校验运动合法性,如果偏差过大最后会到
这里面修正。这个修正的地方也在上面的tick component 里面

        if (bIsClient)
        {
            FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
            if (ClientData && ClientData->bUpdatePosition)
            {
                ClientUpdatePositionAfterServerUpdate();
            }
        }

这里的PendingMove是一个这个结构 FSavedMovePtr,这个是每次移动同步的包的结构。这里还有一系列概念,比如流量优化,多个包合并成等效的一个同步包。

如何自定义网络同步的包结构

有很多地方会调用 GetPredictionData_Client_Character() 获取一个神秘结构
比如这里

        const bool bIsClient = (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client));
        if (bIsClient)
        {
            FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
            if (ClientData && ClientData->bUpdatePosition)
            {
                ClientUpdatePositionAfterServerUpdate();
            }
        }

我们看这个函数具体写了啥

FNetworkPredictionData_Client_Character* UCharacterMovementComponent::GetPredictionData_Client_Character() const
{
    // ...

    if (ClientPredictionData == nullptr)
    {
        UCharacterMovementComponent* MutableThis = const_cast<UCharacterMovementComponent*>(this);
        MutableThis->ClientPredictionData = static_cast<class FNetworkPredictionData_Client_Character*>(GetPredictionData_Client());
    }

    return ClientPredictionData;
}

返回了一个 FNetworkPredictionData_Client_Character 结构,这个结构通过 GetPredictionData_Client 构造,这是一个虚函数,所以FNetworkPredictionData_Client_Character这个结构是可以我们自定义的。
那么我们自定义他有啥用呢?
虚幻每次移动同步的包是通过 FNetworkPredictionData_Client_Character 类中的虚函数AllocateNewMove生成的。

virtual FSavedMovePtr AllocateNewMove();

FSavedMovePtr就是每一次包的结构。
按照这个流程,我们可以指定每次网络同步包的结构

问题解决

到这里我们就明白了 https://github.com/dyanikoglu/ALS-Community 这个项目CharacterMovement的代码是干嘛的了。

我们在自己的 UCharacterMovementComponent 类中实现虚函数 GetPredictionData_Client 来指定我们自己的预测结构 FNetworkPredictionData_Client_Custom

FNetworkPredictionData_Client* UCwlMovement::GetPredictionData_Client() const
{
    check(PawnOwner != nullptr);

    if (!ClientPredictionData)
    {
        UCwlMovement* MutableThis = const_cast<UCwlMovement*>(this);
        MutableThis->ClientPredictionData = new FNetworkPredictionData_Client_Custom(*this);
        MutableThis->ClientPredictionData->MaxSmoothNetUpdateDist = 92.f;
        MutableThis->ClientPredictionData->NoSmoothNetUpdateDist = 140.f;
    }
    return ClientPredictionData;
}

并在这个预测结构中实现 AllocateNewMove 虚函数,就指定好了项目的移动同步包结构

    class FNetworkPredictionData_Client_Custom : public FNetworkPredictionData_Client_Character
    {
    public:
        typedef FNetworkPredictionData_Client_Character Super;

        explicit FNetworkPredictionData_Client_Custom(const UCharacterMovementComponent& ClientMovement);
        virtual FSavedMovePtr AllocateNewMove() override;
    };

最终目的就是把移动包改成我们自己的 FSavedMove_Character=>FCustomSavedMove。以便于我们有机会加点代码

    class FCustomSavedMove : public FSavedMove_Character
    {
    public:
        typedef FSavedMove_Character Super;

        virtual void Clear() override;
        virtual uint8 GetCompressedFlags() const override;
        virtual void SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character & ClientData) override;
        virtual void PrepMoveFor(class ACharacter* Character) override;

        uint8 bSavedRequestMovementSettingsChange : 1;
        ECwlGait SavedAllowedGait = ECwlGait::Walking;
    };

这里有三个函数

uint8 UCwlMovement::FCustomSavedMove::GetCompressedFlags() const
{
    uint8 Result = Super::GetCompressedFlags();

    if (bSavedRequestMovementSettingsChange)
    {
        Result |= FLAG_Custom_0;
    }
    return Result;
}

void UCwlMovement::FCustomSavedMove::SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData)
{
    FSavedMove_Character::SetMoveFor(Character, InDeltaTime, NewAccel, ClientData);

    if (UCwlMovement* CharacterMovement = Cast<UCwlMovement>(Character->GetCharacterMovement()))
    {
        bSavedRequestMovementSettingsChange = CharacterMovement->RequestMovementSettingsChange;
        SavedAllowedGait = CharacterMovement->AllowedGait;
    }
}

void UCwlMovement::FCustomSavedMove::PrepMoveFor(ACharacter* Character)
{
    FSavedMove_Character::PrepMoveFor(Character);

    if (UCwlMovement* CharacterMovement = Cast<UCwlMovement>(Character->GetCharacterMovement()))
    {
        CharacterMovement->AllowedGait = SavedAllowedGait;
    }
}
  • GetCompressedFlags 用于返回一个标记。有一个流量优化,多个移动包可能会合并为一个等效包。而判断能否合并的其中一个条件就是这个flag不一样。我们计一个变量标记,表示有移动速度需要修改,已有修改修塞进移动包的第0位置。避免包的合并导致信息丢失。

  • ReplicateMoveToServer 中有一行调用 SetMoveFor。在发包前客户端的运动组件把需要塞进包里面的东西抄进去

    NewMove->SetMoveFor(CharacterOwner, DeltaTime, NewAcceleration, *ClientData);
  • 在移动组件 TickComponent->ClientUpdatePositionAfterServerUpdate中调用了 PrepMoveFor

    for (int32 i=0; i<ClientData->SavedMoves.Num(); i++)
    {
        FSavedMove_Character* const CurrentMove = ClientData->SavedMoves[i].Get();
        checkSlow(CurrentMove != nullptr);
    
        CurrentMove->PrepMoveFor(CharacterOwner);
    
        // ...
    
        MoveAutonomous(CurrentMove->TimeStamp, CurrentMove->DeltaTime, CurrentMove->GetCompressedFlags(), CurrentMove->Acceleration);
    
        // ...
    }

    收到从服务端回来的包后,客户端可能需要重新更新角色位置,然后遍历了所有未ACK的包,MoveAutonomous中执行了PerformMovement。我认为回滚就是这里执行吧。

完整代码

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Library/CwlCharacterEnumLibrary.h"
#include "Library/CwlCharacterStructLibrary.h"
#include "CwlMovement.generated.h"

/**
 * 
 */
UCLASS()
class JUSTMAKEGAME_API UCwlMovement : public UCharacterMovementComponent
{
    GENERATED_BODY()

    class FCustomSavedMove : public FSavedMove_Character
    {
    public:
        typedef FSavedMove_Character Super;

        virtual void Clear() override;
        virtual uint8 GetCompressedFlags() const override;
        virtual void SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character & ClientData) override;
        virtual void PrepMoveFor(class ACharacter* Character) override;

        uint8 bSavedRequestMovementSettingsChange : 1;
        ECwlGait SavedAllowedGait = ECwlGait::Walking;
    };

    class FNetworkPredictionData_Client_Custom : public FNetworkPredictionData_Client_Character
    {
    public:
        typedef FNetworkPredictionData_Client_Character Super;

        explicit FNetworkPredictionData_Client_Custom(const UCharacterMovementComponent& ClientMovement);
        virtual FSavedMovePtr AllocateNewMove() override;
    };

public:
    explicit UCwlMovement(const FObjectInitializer& ObjectInitializer);
    virtual void BeginPlay() override;
    virtual void OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity) override;
    virtual void UpdateFromCompressedFlags(uint8 Flags) override;
    virtual class FNetworkPredictionData_Client* GetPredictionData_Client() const override;

    virtual void PhysWalking(float DeltaTime, int32 Iterations) override;
    virtual float GetMaxAcceleration() const override;
    virtual float GetMaxBrakingDeceleration() const override;

    float GetMappingSpeed() const;

    UFUNCTION(BlueprintCallable, Category = "Movement Settings")
    void SetMovementSettings(FCwlMovementSettings NewMovementSettings);

    UFUNCTION(BlueprintCallable, Category = "Movement Settings")
    void SetAllowedGait(ECwlGait NewAllowedGait);

    UFUNCTION(Server, Reliable, Category = "Movement Settings")
    void Server_SetAllowedGait(ECwlGait NewAllowedGait);

public:
    UPROPERTY(BlueprintReadOnly, Category = "Cwl|Movement System")
    FCwlMovementSettings CurrentMovementSettings;

protected:
    UPROPERTY()
    uint8 RequestMovementSettingsChange = 1;

    UPROPERTY()
    ECwlGait AllowedGait = ECwlGait::Walking;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "Character/Component/CwlMovement.h"
#include "GameFramework/Character.h"

void UCwlMovement::FCustomSavedMove::Clear()
{
    FSavedMove_Character::Clear();
    bSavedRequestMovementSettingsChange = false;
    SavedAllowedGait = ECwlGait::Walking;
}

uint8 UCwlMovement::FCustomSavedMove::GetCompressedFlags() const
{
    uint8 Result = Super::GetCompressedFlags();

    if (bSavedRequestMovementSettingsChange)
    {
        Result |= FLAG_Custom_0;
    }
    return Result;
}

void UCwlMovement::FCustomSavedMove::SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData)
{
    FSavedMove_Character::SetMoveFor(Character, InDeltaTime, NewAccel, ClientData);

    if (UCwlMovement* CharacterMovement = Cast<UCwlMovement>(Character->GetCharacterMovement()))
    {
        bSavedRequestMovementSettingsChange = CharacterMovement->RequestMovementSettingsChange;
        SavedAllowedGait = CharacterMovement->AllowedGait;
    }
}

void UCwlMovement::FCustomSavedMove::PrepMoveFor(ACharacter* Character)
{
    FSavedMove_Character::PrepMoveFor(Character);

    if (UCwlMovement* CharacterMovement = Cast<UCwlMovement>(Character->GetCharacterMovement()))
    {
        CharacterMovement->AllowedGait = SavedAllowedGait;
    }
}

UCwlMovement::FNetworkPredictionData_Client_Custom::FNetworkPredictionData_Client_Custom(const UCharacterMovementComponent& ClientMovement)
    : Super(ClientMovement)
{
}

FSavedMovePtr UCwlMovement::FNetworkPredictionData_Client_Custom::AllocateNewMove()
{
    return MakeShared<FCustomSavedMove>();
}

UCwlMovement::UCwlMovement(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    BrakingFrictionFactor = 3.f;
}

FNetworkPredictionData_Client* UCwlMovement::GetPredictionData_Client() const
{
    check(PawnOwner != nullptr);

    if (!ClientPredictionData)
    {
        UCwlMovement* MutableThis = const_cast<UCwlMovement*>(this);
        MutableThis->ClientPredictionData = new FNetworkPredictionData_Client_Custom(*this);
        MutableThis->ClientPredictionData->MaxSmoothNetUpdateDist = 92.f;
        MutableThis->ClientPredictionData->NoSmoothNetUpdateDist = 140.f;
    }
    return ClientPredictionData;
}

void UCwlMovement::PhysWalking(float DeltaTime, int32 Iterations)
{
    if (CurrentMovementSettings.MovementCurve)
    {
        GroundFriction = CurrentMovementSettings.MovementCurve->GetVectorValue(GetMappingSpeed()).Z;
    }
    Super::PhysWalking(DeltaTime, Iterations);
}

float UCwlMovement::GetMaxAcceleration() const
{
    if (!IsMovingOnGround() || !CurrentMovementSettings.MovementCurve)
    {
        return Super::GetMaxAcceleration();
    }
    return CurrentMovementSettings.MovementCurve->GetVectorValue(GetMappingSpeed()).X;
}

float UCwlMovement::GetMaxBrakingDeceleration() const
{
    if (!IsMovingOnGround() || !CurrentMovementSettings.MovementCurve)
    {
        return Super::GetMaxBrakingDeceleration();
    }
    return CurrentMovementSettings.MovementCurve->GetVectorValue(GetMappingSpeed()).Y;
}

float UCwlMovement::GetMappingSpeed() const
{
    const float Speed = Velocity.Size2D();
    const float LocalWalkSpeed = CurrentMovementSettings.WalkSpeed;
    const float LocalRunSpeed = CurrentMovementSettings.RunSpeed;
    const float LocalSprintSpeed = CurrentMovementSettings.SprintSpeed;

    float SpeedMappingWalk = FMath::GetMappedRangeValueClamped(FVector2D(0, LocalWalkSpeed), FVector2D(0, 1), Speed);
    float SpeedMappingRun = FMath::GetMappedRangeValueClamped(FVector2D(LocalWalkSpeed, LocalRunSpeed), FVector2D(1, 2), Speed);
    float SpeedMappingSprint = FMath::GetMappedRangeValueClamped(FVector2D(LocalRunSpeed, LocalSprintSpeed), FVector2D(2, 3), Speed);

    if (Speed > LocalRunSpeed)
    {
        return SpeedMappingSprint;
    }
    if (Speed > LocalWalkSpeed)
    {
        return SpeedMappingRun;
    }
    return SpeedMappingWalk;
}

void UCwlMovement::SetMovementSettings(FCwlMovementSettings NewMovementSettings)
{
    CurrentMovementSettings = NewMovementSettings;
    RequestMovementSettingsChange = true;
}

void UCwlMovement::SetAllowedGait(ECwlGait NewAllowedGait)
{
    if (AllowedGait == NewAllowedGait)
    {
        return ;
    }

    if (PawnOwner->IsLocallyControlled())
    {
        AllowedGait = NewAllowedGait;
        if (GetCharacterOwner()->GetLocalRole() == ROLE_AutonomousProxy)
        {
            Server_SetAllowedGait(NewAllowedGait);
        }
        RequestMovementSettingsChange = true;
        return;
    }
    if (!GetCharacterOwner()->HasAuthority())
    {
        const float UpdateMaxWalkSpeed = CurrentMovementSettings.GetSpeedForGait(AllowedGait);
        MaxWalkSpeed = UpdateMaxWalkSpeed;
        MaxWalkSpeedCrouched = UpdateMaxWalkSpeed;
    }
}

void UCwlMovement::Server_SetAllowedGait_Implementation(ECwlGait NewAllowedGait)
{
    AllowedGait = NewAllowedGait;
}

void UCwlMovement::BeginPlay()
{
    Super::BeginPlay();

}

void UCwlMovement::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity)
{
    Super::OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);

    if (!CharacterOwner)
    {
        return ;
    }

    if (RequestMovementSettingsChange)
    {
        const float UpdateMaxWalkSpeed = CurrentMovementSettings.GetSpeedForGait(AllowedGait);
        MaxWalkSpeed = UpdateMaxWalkSpeed;
        MaxWalkSpeedCrouched = UpdateMaxWalkSpeed;
        RequestMovementSettingsChange = false;
    }

}

// client only
void UCwlMovement::UpdateFromCompressedFlags(uint8 Flags)
{
    Super::UpdateFromCompressedFlags(Flags);
    RequestMovementSettingsChange = (Flags & FSavedMove_Character::FLAG_Custom_0) != 0;
}
上一篇
下一篇