一个同步问题
问题的起因是我在做了这个调试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;
}