UE4采用组件化设计思路,一个Actor上拥有多个Component, 这些Component可以是Mesh, Particles,Sound等其他功能性组件, 其中MovementComponent就是属于功能性组件,它对移动算法进行封装,来控制Actor移动。
UMovementComponent
定义了基础的移动辅助功能:
- Restricting movement to a plane or axis.
- Utility functions for special handling of collision results (SlideAlongSurface(), ComputeSlideVector(), TwoWallAdjust()).
- Utility functions for moving when there may be initial penetration (SafeMoveUpdatedComponent(), ResolvePenetration()).
- Automatically registering the component tick and finding a component to move on the owning Actor.
通常MovementComponent控制Actor的RootComponent,在swept移动(非传送式)中,只有UpdatedComponent的碰撞才会被考虑,其它attached的Components的碰撞不考虑,直接设置到最终位置。
class ENGINE_API UMovementComponent : public UActorComponent
{
GENERATED_UCLASS_BODY()
UPROPERTY(BlueprintReadOnly, Transient, DuplicateTransient, Category=MovementComponent)
USceneComponent* UpdatedComponent; //目标Component
/**
* UpdatedComponent, cast as a UPrimitiveComponent. May be invalid if UpdatedComponent was null or not a UPrimitiveComponent.
*/
UPROPERTY(BlueprintReadOnly, Transient, DuplicateTransient, Category=MovementComponent)
UPrimitiveComponent* UpdatedPrimitive;
//~ Begin ActorComponent Interface
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
virtual void RegisterComponentTickFunctions(bool bRegister) override;
virtual void PostLoad() override;
virtual void Deactivate() override;
virtual void Serialize(FArchive& Ar) override;
/** Overridden to auto-register the updated component if it starts NULL, and we can find a root component on our owner. */
virtual void InitializeComponent() override;
/** Overridden to update component properties that should be updated while being edited. */
virtual void OnRegister() override;
//~ End ActorComponent Interface
/** @return gravity that affects this component */
UFUNCTION(BlueprintCallable, Category="Components|Movement")
virtual float GetGravityZ() const;
/** @return Maximum speed of component in current movement mode. */
UFUNCTION(BlueprintCallable, Category="Components|Movement")
virtual float GetMaxSpeed() const;
/**
* Returns true if the current velocity is exceeding the given max speed (usually the result of GetMaxSpeed()), within a small error tolerance.
* Note that under normal circumstances updates cause by acceleration will not cause this to be true, however external forces or changes in the max speed limit
* can cause the max speed to be violated.
*/
UFUNCTION(BlueprintCallable, Category="Components|Movement")
virtual bool IsExceedingMaxSpeed(float MaxSpeed) const;
/** Stops movement immediately (zeroes velocity, usually zeros acceleration for components with acceleration). */
UFUNCTION(BlueprintCallable, Category="Components|Movement")
virtual void StopMovementImmediately();
/** @return PhysicsVolume this MovementComponent is using, or the world's default physics volume if none. **/
UFUNCTION(BlueprintCallable, Category="Components|Movement")
virtual APhysicsVolume* GetPhysicsVolume() const;
/** Delegate when PhysicsVolume of UpdatedComponent has been changed **/
UFUNCTION()
virtual void PhysicsVolumeChanged(class APhysicsVolume* NewVolume);
/** Assign the component we move and update. */
UFUNCTION(BlueprintCallable, Category="Components|Movement")
virtual void SetUpdatedComponent(USceneComponent* NewUpdatedComponent);
/** return true if it's in PhysicsVolume with water flag **/
virtual bool IsInWater() const;
/** Update tick registration state, determined by bAutoUpdateTickRegistration. Called by SetUpdatedComponent. */
virtual void UpdateTickRegistration();
virtual void HandleImpact(const FHitResult& Hit, float TimeSlice=0.f, const FVector& MoveDelta = FVector::ZeroVector);
/** Update ComponentVelocity of UpdatedComponent. This needs to be called by derived classes at the end of an update whenever Velocity has changed. */
virtual void UpdateComponentVelocity();
/** Initialize collision params appropriately based on our collision settings. Use this before any Line, Overlap, or Sweep tests. */
virtual void InitCollisionParams(FCollisionQueryParams &OutParams, FCollisionResponseParams& OutResponseParam) const;
/** Return true if the given collision shape overlaps other geometry at the given location and rotation. The collision params are set by InitCollisionParams(). */
virtual bool OverlapTest(const FVector& Location, const FQuat& RotationQuat, const ECollisionChannel CollisionChannel, const FCollisionShape& CollisionShape, const AActor* IgnoreActor) const;
/**
* Moves our UpdatedComponent by the given Delta, and sets rotation to NewRotation. Respects the plane constraint, if enabled.
* @note This simply calls the virtual MoveUpdatedComponentImpl() which can be overridden to implement custom behavior.
* @note The overload taking rotation as an FQuat is slightly faster than the version using FRotator (which will be converted to an FQuat).
* @note The 'Teleport' flag is currently always treated as 'None' (not teleporting) when used in an active FScopedMovementUpdate.
* @return True if some movement occurred, false if no movement occurred. Result of any impact will be stored in OutHit.
*/
bool MoveUpdatedComponent(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit = NULL, ETeleportType Teleport = ETeleportType::None);
bool MoveUpdatedComponent(const FVector& Delta, const FRotator& NewRotation, bool bSweep, FHitResult* OutHit = NULL, ETeleportType Teleport = ETeleportType::None);
protected:
virtual bool MoveUpdatedComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit = NULL, ETeleportType Teleport = ETeleportType::None);
public:
/**
* Calls MoveUpdatedComponent(), handling initial penetrations by calling ResolvePenetration().
* If this adjustment succeeds, the original movement will be attempted again.
* @note The overload taking rotation as an FQuat is slightly faster than the version using FRotator (which will be converted to an FQuat).
* @note The 'Teleport' flag is currently always treated as 'None' (not teleporting) when used in an active FScopedMovementUpdate.
* @return result of the final MoveUpdatedComponent() call.
*/
bool SafeMoveUpdatedComponent(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult& OutHit, ETeleportType Teleport = ETeleportType::None);
bool SafeMoveUpdatedComponent(const FVector& Delta, const FRotator& NewRotation, bool bSweep, FHitResult& OutHit, ETeleportType Teleport = ETeleportType::None);
/**
* Calculate a movement adjustment to try to move out of a penetration from a failed move.
* @param Hit the result of the failed move
* @return The adjustment to use after a failed move, or a zero vector if no attempt should be made.
*/
virtual FVector GetPenetrationAdjustment(const FHitResult& Hit) const;
/**
* Try to move out of penetration in an object after a failed move. This function should respect the plane constraint if applicable.
* @note This simply calls the virtual ResolvePenetrationImpl() which can be overridden to implement custom behavior.
* @note The overload taking rotation as an FQuat is slightly faster than the version using FRotator (which will be converted to an FQuat)..
* @param Adjustment The requested adjustment, usually from GetPenetrationAdjustment()
* @param Hit The result of the failed move
* @return True if the adjustment was successful and the original move should be retried, or false if no repeated attempt should be made.
*/
bool ResolvePenetration(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotation);
bool ResolvePenetration(const FVector& Adjustment, const FHitResult& Hit, const FRotator& NewRotation);
protected:
virtual bool ResolvePenetrationImpl(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotation);
public:
/**
* Compute a vector to slide along a surface, given an attempted move, time, and normal.
* @param Delta: Attempted move.
* @param Time: Amount of move to apply (between 0 and 1).
* @param Normal: Normal opposed to movement. Not necessarily equal to Hit.Normal.
* @param Hit: HitResult of the move that resulted in the slide.
*/
virtual FVector ComputeSlideVector(const FVector& Delta, const float Time, const FVector& Normal, const FHitResult& Hit) const;
/**
* Slide smoothly along a surface, and slide away from multiple impacts using TwoWallAdjust if necessary. Calls HandleImpact for each surface hit, if requested.
* Uses SafeMoveUpdatedComponent() for movement, and ComputeSlideVector() to determine the slide direction.
* @param Delta: Attempted movement vector.
* @param Time: Percent of Delta to apply (between 0 and 1). Usually equal to the remaining time after a collision: (1.0 - Hit.Time).
* @param Normal: Normal opposing movement, along which we will slide.
* @param Hit: [In] HitResult of the attempted move that resulted in the impact triggering the slide. [Out] HitResult of last attempted move.
* @param bHandleImpact: Whether to call HandleImpact on each hit.
* @return The percentage of requested distance (Delta * Percent) actually applied (between 0 and 1). 0 if no movement occurred, non-zero if movement occurred.
*/
virtual float SlideAlongSurface(const FVector& Delta, float Time, const FVector& Normal, FHitResult &Hit, bool bHandleImpact = false);
/**
* Compute a movement direction when contacting two surfaces.
* @param Delta: [In] Amount of move attempted before impact. [Out] Computed adjustment based on impacts.
* @param Hit: Impact from last attempted move
* @param OldHitNormal: Normal of impact before last attempted move
* @return Result in Delta that is the direction to move when contacting two surfaces.
*/
virtual void TwoWallAdjust(FVector &Delta, const FHitResult& Hit, const FVector &OldHitNormal) const;
/** Called by owning Actor upon successful teleport from AActor::TeleportTo(). */
virtual void OnTeleported() {};
private:
/** Transient flag indicating whether we are executing OnRegister(). */
bool bInOnRegister;
/** Transient flag indicating whether we are executing InitializeComponent(). */
bool bInInitializeComponent;
};
其中
bool UMovementComponent::MoveUpdatedComponentImpl( const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit, ETeleportType Teleport)
{
if (UpdatedComponent)
{
const FVector NewDelta = ConstrainDirectionToPlane(Delta);
return UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, OutHit, MoveComponentFlags, Teleport);
}
return false;
}
下面我们来看一看,在移动过程中OnHit, OverlapBegin, OverlapEnd这些事件是何时产生的,注意UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, OutHit, MoveComponentFlags, Teleport);
,它会调用USceneComponent::MoveComponentImp接口,因为UPrimitiveComponent具有物理碰撞数据,所以我们直接浏览UPrimitiveComponent::MoveComponentImp(),源码在Engine\Source\Runtime\Engine\Private\Components\PrimitiveComponent.cpp
中。
UCharacterMovementComponent
CharacterMovementComponent处理了Character对象的移动逻辑,支持多种移动模式:walking, falling, swimming, flying, custom。移动主要受当前的velocity和acceleration影响。acceleration每帧都会根据input vector被更新。同时提供网络同步功能, 包含server-client校正和预测。
移动算法
在函数void UCharacterMovementComponent::PerformMovement(float DeltaSeconds)
中实现移动功能,
Server-Client移动同步
本节中分析Server-Client同步算法,移动包含如下几点属性:
- 移动位置速度同步
- MoveMode同步(jump, walking, ...)
UE的服务器的设计理念是一切以服务器为准,所有移动位置、战斗的判定都以服务器的结果为准。
考虑如下情形:现有服务器S, 客户端A, 客户端B,客户端A上有个人物Character_A, 在服务器S上的镜像为Character_A_S, 在客户端B上的镜像为Character_A_B,关系图示如下:
当前Character_A为玩家正在操作的人物(控制它移动),那么此时人物的Role, Remote_Role如下表所示
Character | Role | Remote Role |
---|---|---|
Character_A_S | ROLE_Authority | ROLE_AutonomousProxy |
Character_A | ROLE_AutonomousProxy | ROLE_Authority |
Character_A_B | ROLE_SimulatedProxy | ROLE_Authority |
备注:
- ROLE_Authority 表示权威的意思(服务器才有)
- ROLE_AutonomousProxy 表示具有自主操控功能(Client A上被玩家操控)
- ROLE_SimulatedProxy 表示模拟(Client B上的Character_A_B是别的玩家操控的所以只模拟该人物移动)
下面对关键代码进行分析注释
void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
SCOPED_NAMED_EVENT(UCharacterMovementComponent_TickComponent, FColor::Yellow);
SCOPE_CYCLE_COUNTER(STAT_CharacterMovement);
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementTick);
const FVector InputVector = ConsumeInputVector();
if (!HasValidData() || ShouldSkipUpdate(DeltaTime))
{
return;
}
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Super tick may destroy/invalidate CharacterOwner or UpdatedComponent, so we need to re-check.
if (!HasValidData())
{
return;
}
// See if we fell out of the world.
const bool bIsSimulatingPhysics = UpdatedComponent->IsSimulatingPhysics();
if (CharacterOwner->Role == ROLE_Authority && (!bCheatFlying || bIsSimulatingPhysics) && !CharacterOwner->CheckStillInWorld())
{
return;
}
// We don't update if simulating physics (eg ragdolls).
// 如果此时使用物理引擎的模拟驱动人物移动(人物死后的布娃娃动画)
if (bIsSimulatingPhysics)
{
// Update camera to ensure client gets updates even when physics move him far away from point where simulation started
if (CharacterOwner->Role == ROLE_AutonomousProxy && IsNetMode(NM_Client))
{
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
APlayerCameraManager* PlayerCameraManager = (PC ? PC->PlayerCameraManager : NULL);
if (PlayerCameraManager != NULL && PlayerCameraManager->bUseClientSideCameraUpdates)
{
PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;
}
}
ClearAccumulatedForces();
return;
}
AvoidanceLockTimer -= DeltaTime;
if (CharacterOwner->Role > 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->Role == ROLE_AutonomousProxy && IsNetMode(NM_Client));
if (bIsClient)
{
// 如果是客户端并且是ROLE_AutonomousProxy, 需要处理服务器发来的移动校正信息
ClientUpdatePositionAfterServerUpdate();
}
// Allow root motion to move characters that have no controller.
if( CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()) )
{ // 如果是本地玩家控制(在listen模式下,此时游戏既作为服务器又作为为客户端)
{
SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);
// We need to check the jump state before adjusting input acceleration, to minimize latency
// and to make sure acceleration respects our potentially new falling state.
CharacterOwner->CheckJumpInput(DeltaTime);
// apply input to acceleration
Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));
AnalogInputModifier = ComputeAnalogInputModifier();
}
if (CharacterOwner->Role == ROLE_Authority)
{
PerformMovement(DeltaTime); // 直接执行PerformMovement
}
else if (bIsClient)
{
ReplicateMoveToServer(DeltaTime, Acceleration); // 此时在客户端上,玩家控制人物,调用该函数处理移动请求。
}
}
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->Role == ROLE_SimulatedProxy)
{
if (bShrinkProxyCapsule)
{
AdjustProxyCapsuleSize();
}
SimulatedTick(DeltaTime); // 客户端纯simulate人物移动
}
if (bUseRVOAvoidance)
{
UpdateDefaultAvoidance();
}
if (bEnablePhysicsInteraction)
{
SCOPE_CYCLE_COUNTER(STAT_CharPhysicsInteraction);
ApplyDownwardForce(DeltaTime);
ApplyRepulsionForce(DeltaTime);
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
const bool bVisualizeMovement = CharacterMovementCVars::VisualizeMovement > 0;
if (bVisualizeMovement)
{
VisualizeMovement();
}
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
}
下面分类讨论移动执行:
- 在客户端A上, 操作Character_A
此时执行ReplicateMoveToServer()。
void UCharacterMovementComponent::ReplicateMoveToServer(float DeltaTime, const FVector& NewAcceleration)
{
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementReplicateMoveToServer);
check(CharacterOwner != NULL);
// Can only start sending moves if our controllers are synced up over the network, otherwise we flood the reliable buffer.
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
if (PC && PC->AcknowledgedPawn != CharacterOwner)
{
return;
}
// Bail out if our character's controller doesn't have a Player. This may be the case when the local player
// has switched to another controller, such as a debug camera controller.
if (PC && PC->Player == nullptr)
{
return;
}
// 分配客户端预测数据结构
FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
if (!ClientData)
{
return;
}
// Update our delta time for physics simulation.
DeltaTime = ClientData->UpdateTimeStampAndDeltaTime(DeltaTime, *CharacterOwner, *this);
// Find the oldest (unacknowledged) important move (OldMove).
// Don't include the last move because it may be combined with the next new move.
// A saved move is interesting if it differs significantly from the last acknowledged move
FSavedMovePtr OldMove = NULL;
if( ClientData->LastAckedMove.IsValid() )
{
const int32 NumSavedMoves = ClientData->SavedMoves.Num();
for (int32 i=0; i < NumSavedMoves-1; i++)
{
const FSavedMovePtr& CurrentMove = ClientData->SavedMoves[i];
if (CurrentMove->IsImportantMove(ClientData->LastAckedMove))
{
OldMove = CurrentMove;
break;
}
}
}
// Get a SavedMove object to store the movement in.
FSavedMovePtr NewMove = ClientData->CreateSavedMove(); // 分配新的SavedMove项
if (NewMove.IsValid() == false)
{
return;
}
NewMove->SetMoveFor(CharacterOwner, DeltaTime, NewAcceleration, *ClientData);
// see if the two moves could be combined
// do not combine moves which have different TimeStamps (before and after reset).
if( ClientData->PendingMove.IsValid() && !ClientData->PendingMove->bOldTimeStampBeforeReset && ClientData->PendingMove->CanCombineWith(NewMove, CharacterOwner, ClientData->MaxMoveDeltaTime * CharacterOwner->GetActorTimeDilation()))
{
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementCombineNetMove);
// Only combine and move back to the start location if we don't move back in to a spot that would make us collide with something new.
const FVector OldStartLocation = ClientData->PendingMove->GetRevertedLocation();
if (!OverlapTest(OldStartLocation, ClientData->PendingMove->StartRotation.Quaternion(), UpdatedComponent->GetCollisionObjectType(), GetPawnCapsuleCollisionShape(SHRINK_None), CharacterOwner))
{
FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, EScopedUpdate::DeferredUpdates);
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("CombineMove: add delta %f + %f and revert from %f %f to %f %f"), DeltaTime, ClientData->PendingMove->DeltaTime, UpdatedComponent->GetComponentLocation().X, UpdatedComponent->GetComponentLocation().Y, OldStartLocation.X, OldStartLocation.Y);
// to combine move, first revert pawn position to PendingMove start position, before playing combined move on client
const bool bNoCollisionCheck = true;
UpdatedComponent->SetWorldLocationAndRotation(OldStartLocation, ClientData->PendingMove->StartRotation, false);
Velocity = ClientData->PendingMove->StartVelocity;
SetBase(ClientData->PendingMove->StartBase.Get(), ClientData->PendingMove->StartBoneName);
CurrentFloor = ClientData->PendingMove->StartFloor;
// Now that we have reverted to the old position, prepare a new move from that position,
// using our current velocity, acceleration, and rotation, but applied over the combined time from the old and new move.
NewMove->DeltaTime += ClientData->PendingMove->DeltaTime;
if (PC)
{
// We reverted position to that at the start of the pending move (above), however some code paths expect rotation to be set correctly
// before character movement occurs (via FaceRotation), so try that now. The bOrientRotationToMovement path happens later as part of PerformMovement() and PhysicsRotation().
CharacterOwner->FaceRotation(PC->GetControlRotation(), NewMove->DeltaTime);
}
SaveBaseLocation();
NewMove->SetInitialPosition(CharacterOwner);
// Remove pending move from move list. It would have to be the last move on the list.
if (ClientData->SavedMoves.Num() > 0 && ClientData->SavedMoves.Last() == ClientData->PendingMove)
{
const bool bAllowShrinking = false;
ClientData->SavedMoves.Pop(bAllowShrinking);
}
ClientData->FreeMove(ClientData->PendingMove);
ClientData->PendingMove = NULL;
}
else
{
//UE_LOG(LogNet, Log, TEXT("Not combining move, would collide at start location"));
}
}
// Acceleration should match what we send to the server, plus any other restrictions the server also enforces (see MoveAutonomous).
Acceleration = NewMove->Acceleration.GetClampedToMaxSize(GetMaxAcceleration());
AnalogInputModifier = ComputeAnalogInputModifier(); // recompute since acceleration may have changed.
// Perform the move locally
CharacterOwner->ClientRootMotionParams.Clear();
CharacterOwner->SavedRootMotion.Clear();
PerformMovement(NewMove->DeltaTime); //执行本地Movement
NewMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Record);
// Add NewMove to the list
if (CharacterOwner->bReplicateMovement)
{
ClientData->SavedMoves.Push(NewMove);
const UWorld* MyWorld = GetWorld();
const bool bCanDelayMove = (CharacterMovementCVars::NetEnableMoveCombining != 0) && CanDelaySendingMove(NewMove);
if (bCanDelayMove && ClientData->PendingMove.IsValid() == false)
{
// Decide whether to hold off on move
const float NetMoveDelta = FMath::Clamp(GetClientNetSendDeltaTime(PC, ClientData, NewMove), 1.f/120.f, 1.f/15.f);
if ((MyWorld->TimeSeconds - ClientData->ClientUpdateTime) * MyWorld->GetWorldSettings()->GetEffectiveTimeDilation() < NetMoveDelta)
{
// Delay sending this move.
ClientData->PendingMove = NewMove;
return;
}
}
ClientData->ClientUpdateTime = MyWorld->TimeSeconds;
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("Client ReplicateMove Time %f Acceleration %s Position %s DeltaTime %f"),
NewMove->TimeStamp, *NewMove->Acceleration.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), DeltaTime);
// Send move to server if this character is replicating movement
// 发起RPC调用,请求服务器执行NewMove命令
{
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementCallServerMove);
CallServerMove(NewMove.Get(), OldMove.Get());
}
}
ClientData->PendingMove = NULL;
}
void UCharacterMovementComponent::CallServerMove
(
const class FSavedMove_Character* NewMove,
const class FSavedMove_Character* OldMove
)
{
check(NewMove != NULL);
// Compress rotation down to 5 bytes
const uint32 ClientYawPitchINT = PackYawAndPitchTo32(NewMove->SavedControlRotation.Yaw, NewMove->SavedControlRotation.Pitch);
const uint8 ClientRollBYTE = FRotator::CompressAxisToByte(NewMove->SavedControlRotation.Roll);
// Determine if we send absolute or relative location
UPrimitiveComponent* ClientMovementBase = NewMove->EndBase.Get();
const FName ClientBaseBone = NewMove->EndBoneName;
const FVector SendLocation = MovementBaseUtility::UseRelativeLocation(ClientMovementBase) ? NewMove->SavedRelativeLocation : NewMove->SavedLocation;
// send old move if it exists
if (OldMove)
{
ServerMoveOld(OldMove->TimeStamp, OldMove->Acceleration, OldMove->GetCompressedFlags());
}
FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
if (ClientData->PendingMove.IsValid())
{
const uint32 OldClientYawPitchINT = PackYawAndPitchTo32(ClientData->PendingMove->SavedControlRotation.Yaw, ClientData->PendingMove->SavedControlRotation.Pitch);
// If we delayed a move without root motion, and our new move has root motion, send these through a special function, so the server knows how to process them.
if ((ClientData->PendingMove->RootMotionMontage == NULL) && (NewMove->RootMotionMontage != NULL))
{
// send two moves simultaneously
ServerMoveDualHybridRootMotion
(
ClientData->PendingMove->TimeStamp,
ClientData->PendingMove->Acceleration,
ClientData->PendingMove->GetCompressedFlags(),
OldClientYawPitchINT,
NewMove->TimeStamp,
NewMove->Acceleration,
SendLocation,
NewMove->GetCompressedFlags(),
ClientRollBYTE,
ClientYawPitchINT,
ClientMovementBase,
ClientBaseBone,
NewMove->MovementMode
);
}
else
{
// send two moves simultaneously
ServerMoveDual
(
ClientData->PendingMove->TimeStamp,
ClientData->PendingMove->Acceleration,
ClientData->PendingMove->GetCompressedFlags(),
OldClientYawPitchINT,
NewMove->TimeStamp,
NewMove->Acceleration,
SendLocation,
NewMove->GetCompressedFlags(),
ClientRollBYTE,
ClientYawPitchINT,
ClientMovementBase,
ClientBaseBone,
NewMove->MovementMode
);
}
}
else
{
ServerMove
(
NewMove->TimeStamp,
NewMove->Acceleration,
SendLocation,
NewMove->GetCompressedFlags(),
ClientRollBYTE,
ClientYawPitchINT,
ClientMovementBase,
ClientBaseBone,
NewMove->MovementMode
);
}
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
APlayerCameraManager* PlayerCameraManager = (PC ? PC->PlayerCameraManager : NULL);
if (PlayerCameraManager != NULL && PlayerCameraManager->bUseClientSideCameraUpdates)
{
PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;
}
}
上面的客户端执行的流程为:
- 申请一个SavedMove项NewMove, 填写当前的移动命令(加速度、Jump/Croud、Postion、Pose、MovementMode、timeStamp等);
- 本地执行PerformMovement();
- 将NewMove放入移动命令列表中;
- 发起远程调用 CallServerMove(),请求服务器执行移动命令
- 在服务器S上, 处理客户端A的ServerMove请求
void UCharacterMovementComponent::ServerMove_Implementation(
float TimeStamp,
FVector_NetQuantize10 InAccel,
FVector_NetQuantize100 ClientLoc,
uint8 MoveFlags,
uint8 ClientRoll,
uint32 View,
UPrimitiveComponent* ClientMovementBase,
FName ClientBaseBoneName,
uint8 ClientMovementMode)
{
if (!HasValidData() || !IsActive())
{
return;
}
FNetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character();
check(ServerData);
if( !VerifyClientTimeStamp(TimeStamp, *ServerData) )
{
return;
}
bool bServerReadyForClient = true;
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
if (PC)
{
bServerReadyForClient = PC->NotifyServerReceivedClientData(CharacterOwner, TimeStamp);
if (!bServerReadyForClient)
{
InAccel = FVector::ZeroVector;
}
}
// View components
const uint16 ViewPitch = (View & 65535);
const uint16 ViewYaw = (View >> 16);
const FVector Accel = InAccel;
// Save move parameters.
const float DeltaTime = ServerData->GetServerMoveDeltaTime(TimeStamp, CharacterOwner->GetActorTimeDilation());
ServerData->CurrentClientTimeStamp = TimeStamp; // 客户端的发送Movement时的时间戳
ServerData->ServerTimeStamp = GetWorld()->GetTimeSeconds();
ServerData->ServerTimeStampLastServerMove = ServerData->ServerTimeStamp;
FRotator ViewRot;
ViewRot.Pitch = FRotator::DecompressAxisFromShort(ViewPitch);
ViewRot.Yaw = FRotator::DecompressAxisFromShort(ViewYaw);
ViewRot.Roll = FRotator::DecompressAxisFromByte(ClientRoll);
if (PC)
{
PC->SetControlRotation(ViewRot);
}
if (!bServerReadyForClient)
{
return;
}
// Perform actual movement
if ((GetWorld()->GetWorldSettings()->Pauser == NULL) && (DeltaTime > 0.f))
{
if (PC)
{
PC->UpdateRotation(DeltaTime);
}
// 执行客户端的请求
MoveAutonomous(TimeStamp, DeltaTime, MoveFlags, Accel);
}
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ServerMove Time %f Acceleration %s Position %s DeltaTime %f"),
TimeStamp, *Accel.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), DeltaTime);
// 服务器判断客户端是否出现大的执行误差
ServerMoveHandleClientError(TimeStamp, DeltaTime, Accel, ClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode);
}
// 服务器执行客户端的移动请求
void UCharacterMovementComponent::MoveAutonomous
(
float ClientTimeStamp,
float DeltaTime,
uint8 CompressedFlags,
const FVector& NewAccel
)
{
if (!HasValidData())
{
return;
}
UpdateFromCompressedFlags(CompressedFlags); // 设置Jump, Cround标记
CharacterOwner->CheckJumpInput(DeltaTime);
Acceleration = ConstrainInputAcceleration(NewAccel);
Acceleration = Acceleration.GetClampedToMaxSize(GetMaxAcceleration());
AnalogInputModifier = ComputeAnalogInputModifier();
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
const FQuat OldRotation = UpdatedComponent->GetComponentQuat();
PerformMovement(DeltaTime); // 执行移动算法
// Check if data is valid as PerformMovement can mark character for pending kill
if (!HasValidData())
{
return;
}
// If not playing root motion, tick animations after physics. We do this here to keep events, notifies, states and transitions in sync with client updates.
if( CharacterOwner && !CharacterOwner->bClientUpdating && !CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh() )
{
TickCharacterPose(DeltaTime);
// TODO: SaveBaseLocation() in case tick moves us?
// Trigger Events right away, as we could be receiving multiple ServerMoves per frame.
CharacterOwner->GetMesh()->ConditionallyDispatchQueuedAnimEvents();
}
if (CharacterOwner && UpdatedComponent)
{
// Smooth local view of remote clients on listen servers
if (CharacterMovementCVars::NetEnableListenServerSmoothing &&
CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy &&
IsNetMode(NM_ListenServer))
{
SmoothCorrection(OldLocation, OldRotation, UpdatedComponent->GetComponentLocation(), UpdatedComponent->GetComponentQuat());
}
}
}
void UCharacterMovementComponent::ServerMoveHandleClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& RelativeClientLoc, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode)
{
if (RelativeClientLoc == FVector(1.f,2.f,3.f)) // first part of double servermove
{
return;
}
FNetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character();
check(ServerData);
// Don't prevent more recent updates from being sent if received this frame.
// We're going to send out an update anyway, might as well be the most recent one.
APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());
if( (ServerData->LastUpdateTime != GetWorld()->TimeSeconds) && GetDefault<AGameNetworkManager>()->WithinUpdateDelayBounds(PC, ServerData->LastUpdateTime))
{
return;
}
// Offset may be relative to base component
FVector ClientLoc = RelativeClientLoc;
if (MovementBaseUtility::UseRelativeLocation(ClientMovementBase))
{
FVector BaseLocation;
FQuat BaseRotation;
MovementBaseUtility::GetMovementBaseTransform(ClientMovementBase, ClientBaseBoneName, BaseLocation, BaseRotation);
ClientLoc += BaseLocation;
}
// Compute the client error from the server's position
// If client has accumulated a noticeable positional error, correct him.
if (ServerData->bForceClientUpdate || ServerCheckClientError(ClientTimeStamp, DeltaTime, Accel, ClientLoc, RelativeClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode))
{
UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase();
ServerData->PendingAdjustment.NewVel = Velocity;
ServerData->PendingAdjustment.NewBase = MovementBase;
ServerData->PendingAdjustment.NewBaseBoneName = CharacterOwner->GetBasedMovement().BoneName;
ServerData->PendingAdjustment.NewLoc = FRepMovement::RebaseOntoZeroOrigin(UpdatedComponent->GetComponentLocation(), this);
ServerData->PendingAdjustment.NewRot = UpdatedComponent->GetComponentRotation();
ServerData->PendingAdjustment.bBaseRelativePosition = MovementBaseUtility::UseRelativeLocation(MovementBase);
if (ServerData->PendingAdjustment.bBaseRelativePosition)
{
// Relative location
ServerData->PendingAdjustment.NewLoc = CharacterOwner->GetBasedMovement().Location;
// TODO: this could be a relative rotation, but all client corrections ignore rotation right now except the root motion one, which would need to be updated.
//ServerData->PendingAdjustment.NewRot = CharacterOwner->GetBasedMovement().Rotation;
}
#if !UE_BUILD_SHIPPING
if (CharacterMovementCVars::NetShowCorrections != 0)
{
const FVector LocDiff = UpdatedComponent->GetComponentLocation() - ClientLoc;
const FString BaseString = MovementBase ? MovementBase->GetPathName(MovementBase->GetOutermost()) : TEXT("None");
UE_LOG(LogNetPlayerMovement, Warning, TEXT("*** Server: Error for %s at Time=%.3f is %3.3f LocDiff(%s) ClientLoc(%s) ServerLoc(%s) Base: %s Bone: %s Accel(%s) Velocity(%s)"),
*GetNameSafe(CharacterOwner), ClientTimeStamp, LocDiff.Size(), *LocDiff.ToString(), *ClientLoc.ToString(), *UpdatedComponent->GetComponentLocation().ToString(), *BaseString, *ServerData->PendingAdjustment.NewBaseBoneName.ToString(), *Accel.ToString(), *Velocity.ToString());
const float DebugLifetime = CharacterMovementCVars::NetCorrectionLifetime;
DrawDebugCapsule(GetWorld(), UpdatedComponent->GetComponentLocation(), CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(100, 255, 100), true, DebugLifetime);
DrawDebugCapsule(GetWorld(), ClientLoc , CharacterOwner->GetSimpleCollisionHalfHeight(), CharacterOwner->GetSimpleCollisionRadius(), FQuat::Identity, FColor(255, 100, 100), true, DebugLifetime);
}
#endif
ServerData->LastUpdateTime = GetWorld()->TimeSeconds;
ServerData->PendingAdjustment.DeltaTime = DeltaTime;
ServerData->PendingAdjustment.TimeStamp = ClientTimeStamp; // 客户端NewMove的时间戳
ServerData->PendingAdjustment.bAckGoodMove = false; // 设置标记,客户端当前出现误差
ServerData->PendingAdjustment.MovementMode = PackNetworkMovementMode();
PerfCountersIncrement(TEXT("NumServerMoveCorrections"));
}
else
{
if (GetDefault<AGameNetworkManager>()->ClientAuthorativePosition)
{
const FVector LocDiff = UpdatedComponent->GetComponentLocation() - ClientLoc; //-V595
if (!LocDiff.IsZero() || ClientMovementMode != PackNetworkMovementMode() || GetMovementBase() != ClientMovementBase || (CharacterOwner && CharacterOwner->GetBasedMovement().BoneName != ClientBaseBoneName))
{
// Just set the position. On subsequent moves we will resolve initially overlapping conditions.
UpdatedComponent->SetWorldLocation(ClientLoc, false); //-V595
// Trust the client's movement mode.
ApplyNetworkMovementMode(ClientMovementMode);
// Update base and floor at new location.
SetBase(ClientMovementBase, ClientBaseBoneName);
UpdateFloorFromAdjustment();
// Even if base has not changed, we need to recompute the relative offsets (since we've moved).
SaveBaseLocation();
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector;
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity;
LastUpdateVelocity = Velocity;
}
}
// acknowledge receipt of this successful servermove()
ServerData->PendingAdjustment.TimeStamp = ClientTimeStamp; // 客户端NewMove的时间戳
ServerData->PendingAdjustment.bAckGoodMove = true; // 客户端没有出现误差
}
PerfCountersIncrement(TEXT("NumServerMoves"));
ServerData->bForceClientUpdate = false;
}
bool UCharacterMovementComponent::ServerCheckClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode)
{
// Check location difference against global setting
if (!bIgnoreClientMovementErrorChecksAndCorrection)
{
const FVector LocDiff = UpdatedComponent->GetComponentLocation() - ClientWorldLocation;
#if ROOT_MOTION_DEBUG
if (RootMotionSourceDebug::CVarDebugRootMotionSources.GetValueOnAnyThread() == 1)
{
FString AdjustedDebugString = FString::Printf(TEXT("ServerCheckClientError LocDiff(%.1f) ExceedsAllowablePositionError(%d) TimeStamp(%f)"),
LocDiff.Size(), GetDefault<AGameNetworkManager>()->ExceedsAllowablePositionError(LocDiff), ClientTimeStamp);
RootMotionSourceDebug::PrintOnScreen(*CharacterOwner, AdjustedDebugString);
}
#endif
if (GetDefault<AGameNetworkManager>()->ExceedsAllowablePositionError(LocDiff)) // 是否已超出误差范围
{
return true;
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if (CharacterMovementCVars::NetForceClientAdjustmentPercent > SMALL_NUMBER)
{
if (FMath::SRand() < CharacterMovementCVars::NetForceClientAdjustmentPercent) // 有几率地强制校正
{
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("** ServerCheckClientError forced by p.NetForceClientAdjustmentPercent"));
return true;
}
}
#endif
}
else
{
#if !UE_BUILD_SHIPPING
if (CharacterMovementCVars::NetShowCorrections != 0)
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("*** Server: %s is set to ignore error checks and corrections."), *GetNameSafe(CharacterOwner));
}
#endif // !UE_BUILD_SHIPPING
}
// Check for disagreement in movement mode
const uint8 CurrentPackedMovementMode = PackNetworkMovementMode();
if (CurrentPackedMovementMode != ClientMovementMode) // Movement不同
{
return true;
}
return false;
}
服务器会调用SendClientAdjustment()来通知客户端NewMove的执行结果
void UCharacterMovementComponent::SendClientAdjustment()
{
if (!HasValidData())
{
return;
}
FNetworkPredictionData_Server_Character* ServerData = GetPredictionData_Server_Character();
check(ServerData);
if (ServerData->PendingAdjustment.TimeStamp <= 0.f)
{
return;
}
if (ServerData->PendingAdjustment.bAckGoodMove == true)
{
// just notify client this move was received
ClientAckGoodMove(ServerData->PendingAdjustment.TimeStamp); // 客户端NewMove的时间戳
}
else
{
const bool bIsPlayingNetworkedRootMotionMontage = CharacterOwner->IsPlayingNetworkedRootMotionMontage();
if( HasRootMotionSources() )
{
FRotator Rotation = ServerData->PendingAdjustment.NewRot.GetNormalized();
FVector_NetQuantizeNormal CompressedRotation(Rotation.Pitch / 180.f, Rotation.Yaw / 180.f, Rotation.Roll / 180.f);
ClientAdjustRootMotionSourcePosition
(
ServerData->PendingAdjustment.TimeStamp,
CurrentRootMotion,
bIsPlayingNetworkedRootMotionMontage,
bIsPlayingNetworkedRootMotionMontage ? CharacterOwner->GetRootMotionAnimMontageInstance()->GetPosition() : -1.f,
ServerData->PendingAdjustment.NewLoc,
CompressedRotation,
ServerData->PendingAdjustment.NewVel.Z,
ServerData->PendingAdjustment.NewBase,
ServerData->PendingAdjustment.NewBaseBoneName,
ServerData->PendingAdjustment.NewBase != NULL,
ServerData->PendingAdjustment.bBaseRelativePosition,
PackNetworkMovementMode()
);
}
else if( bIsPlayingNetworkedRootMotionMontage )
{
FRotator Rotation = ServerData->PendingAdjustment.NewRot.GetNormalized();
FVector_NetQuantizeNormal CompressedRotation(Rotation.Pitch / 180.f, Rotation.Yaw / 180.f, Rotation.Roll / 180.f);
ClientAdjustRootMotionPosition
(
ServerData->PendingAdjustment.TimeStamp,
CharacterOwner->GetRootMotionAnimMontageInstance()->GetPosition(),
ServerData->PendingAdjustment.NewLoc,
CompressedRotation,
ServerData->PendingAdjustment.NewVel.Z,
ServerData->PendingAdjustment.NewBase,
ServerData->PendingAdjustment.NewBaseBoneName,
ServerData->PendingAdjustment.NewBase != NULL,
ServerData->PendingAdjustment.bBaseRelativePosition,
PackNetworkMovementMode()
);
}
else if (ServerData->PendingAdjustment.NewVel.IsZero())
{
ClientVeryShortAdjustPosition
(
ServerData->PendingAdjustment.TimeStamp,
ServerData->PendingAdjustment.NewLoc,
ServerData->PendingAdjustment.NewBase,
ServerData->PendingAdjustment.NewBaseBoneName,
ServerData->PendingAdjustment.NewBase != NULL,
ServerData->PendingAdjustment.bBaseRelativePosition,
PackNetworkMovementMode()
);
}
else
{
ClientAdjustPosition
(
ServerData->PendingAdjustment.TimeStamp,
ServerData->PendingAdjustment.NewLoc,
ServerData->PendingAdjustment.NewVel,
ServerData->PendingAdjustment.NewBase,
ServerData->PendingAdjustment.NewBaseBoneName,
ServerData->PendingAdjustment.NewBase != NULL,
ServerData->PendingAdjustment.bBaseRelativePosition,
PackNetworkMovementMode()
);
}
}
ServerData->PendingAdjustment.TimeStamp = 0;
ServerData->PendingAdjustment.bAckGoodMove = false;
ServerData->bForceClientUpdate = false;
}
-
客户端A收到服务器的移动反馈
- 收到AckGoodMove
void UCharacterMovementComponent::ClientAckGoodMove_Implementation(float TimeStamp)
{
if (!HasValidData() || !IsActive())
{
return;
}
FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
check(ClientData);
#if ROOT_MOTION_DEBUG
if (RootMotionSourceDebug::CVarDebugRootMotionSources.GetValueOnAnyThread() == 1)
{
FString AdjustedDebugString = FString::Printf(TEXT("ClientAckGoodMove_Implementation TimeStamp(%f)"),
TimeStamp);
RootMotionSourceDebug::PrintOnScreen(*CharacterOwner, AdjustedDebugString);
}
#endif
// Ack move if it has not expired.
int32 MoveIndex = ClientData->GetSavedMoveIndex(TimeStamp);
if( MoveIndex == INDEX_NONE )
{
if( ClientData->LastAckedMove.IsValid() )
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("ClientAckGoodMove_Implementation could not find Move for TimeStamp: %f, LastAckedTimeStamp: %f, CurrentTimeStamp: %f"), TimeStamp, ClientData->LastAckedMove->TimeStamp, ClientData->CurrentTimeStamp);
}
return;
}
// 移除掉timestamp之前的所以Move记录
ClientData->AckMove(MoveIndex);
}
- 收到Adjust命令
void UCharacterMovementComponent::ClientAdjustPosition_Implementation
(
float TimeStamp,
FVector NewLocation,
FVector NewVelocity,
UPrimitiveComponent* NewBase,
FName NewBaseBoneName,
bool bHasBase,
bool bBaseRelativePosition,
uint8 ServerMovementMode
)
{
if (!HasValidData() || !IsActive())
{
return;
}
FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
check(ClientData);
// Make sure the base actor exists on this client.
const bool bUnresolvedBase = bHasBase && (NewBase == NULL);
if (bUnresolvedBase)
{
if (bBaseRelativePosition)
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("ClientAdjustPosition_Implementation could not resolve the new relative movement base actor, ignoring server correction!"));
return;
}
else
{
UE_LOG(LogNetPlayerMovement, Verbose, TEXT("ClientAdjustPosition_Implementation could not resolve the new absolute movement base actor, but WILL use the position!"));
}
}
// Ack move if it has not expired.
int32 MoveIndex = ClientData->GetSavedMoveIndex(TimeStamp);
if( MoveIndex == INDEX_NONE )
{
if( ClientData->LastAckedMove.IsValid() )
{
UE_LOG(LogNetPlayerMovement, Log, TEXT("ClientAdjustPosition_Implementation could not find Move for TimeStamp: %f, LastAckedTimeStamp: %f, CurrentTimeStamp: %f"), TimeStamp, ClientData->LastAckedMove->TimeStamp, ClientData->CurrentTimeStamp);
}
return;
}
ClientData->AckMove(MoveIndex); // 移除掉Timestamp之前的Move记录
FVector WorldShiftedNewLocation;
// Received Location is relative to dynamic base
if (bBaseRelativePosition)
{
FVector BaseLocation;
FQuat BaseRotation;
MovementBaseUtility::GetMovementBaseTransform(NewBase, NewBaseBoneName, BaseLocation, BaseRotation); // TODO: error handling if returns false
WorldShiftedNewLocation = NewLocation + BaseLocation;
}
else
{
WorldShiftedNewLocation = FRepMovement::RebaseOntoLocalOrigin(NewLocation, this);
}
// Trigger event
OnClientCorrectionReceived(*ClientData, TimeStamp, NewLocation, NewVelocity, NewBase, NewBaseBoneName, bHasBase, bBaseRelativePosition, ServerMovementMode);
// Trust the server's positioning.
UpdatedComponent->SetWorldLocation(WorldShiftedNewLocation, false);
Velocity = NewVelocity;
// Trust the server's movement mode
UPrimitiveComponent* PreviousBase = CharacterOwner->GetMovementBase();
ApplyNetworkMovementMode(ServerMovementMode);
// Set base component
UPrimitiveComponent* FinalBase = NewBase;
FName FinalBaseBoneName = NewBaseBoneName;
if (bUnresolvedBase)
{
check(NewBase == NULL);
check(!bBaseRelativePosition);
// We had an unresolved base from the server
// If walking, we'd like to continue walking if possible, to avoid falling for a frame, so try to find a base where we moved to.
if (PreviousBase)
{
FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, false); //-V595
if (CurrentFloor.IsWalkableFloor())
{
FinalBase = CurrentFloor.HitResult.Component.Get();
FinalBaseBoneName = CurrentFloor.HitResult.BoneName;
}
else
{
FinalBase = nullptr;
FinalBaseBoneName = NAME_None;
}
}
}
SetBase(FinalBase, FinalBaseBoneName);
// Update floor at new location
UpdateFloorFromAdjustment();
bJustTeleported = true;
// Even if base has not changed, we need to recompute the relative offsets (since we've moved).
SaveBaseLocation();
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector;
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity;
LastUpdateVelocity = Velocity;
UpdateComponentVelocity();
ClientData->bUpdatePosition = true; // 标记需要ClientUpdatePosition
}
之前在TickComponent()中有调用ClientUpdatePositionAfterServerUpdate
bool UCharacterMovementComponent::ClientUpdatePositionAfterServerUpdate()
{
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementClientUpdatePositionAfterServerUpdate);
if (!HasValidData())
{
return false;
}
FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
check(ClientData);
if (!ClientData->bUpdatePosition) // 检查收到校正通知时设置的标记
{
return false;
}
if (bIgnoreClientMovementErrorChecksAndCorrection)
{
#if !UE_BUILD_SHIPPING
if (CharacterMovementCVars::NetShowCorrections != 0)
{
UE_LOG(LogNetPlayerMovement, Warning, TEXT("*** Client: %s is set to ignore error checks and corrections with %d saved moves in queue."), *GetNameSafe(CharacterOwner), ClientData->SavedMoves.Num());
}
#endif // !UE_BUILD_SHIPPING
return false;
}
ClientData->bUpdatePosition = false;
// Don't do any network position updates on things running PHYS_RigidBody
if (CharacterOwner->GetRootComponent() && CharacterOwner->GetRootComponent()->IsSimulatingPhysics())
{
return false;
}
if (ClientData->SavedMoves.Num() == 0)
{
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("ClientUpdatePositionAfterServerUpdate No saved moves to replay"), ClientData->SavedMoves.Num());
// With no saved moves to resimulate, the move the server updated us with is the last move we've done, no resimulation needed.
CharacterOwner->bClientResimulateRootMotion = false;
if (CharacterOwner->bClientResimulateRootMotionSources)
{
// With no resimulation, we just update our current root motion to what the server sent us
UE_LOG(LogRootMotion, VeryVerbose, TEXT("CurrentRootMotion getting updated to ServerUpdate state: %s"), *CharacterOwner->GetName());
CurrentRootMotion.UpdateStateFrom(CharacterOwner->SavedRootMotion);
CharacterOwner->bClientResimulateRootMotionSources = false;
}
return false;
}
// Save important values that might get affected by the replay.
const float SavedAnalogInputModifier = AnalogInputModifier;
const FRootMotionMovementParams BackupRootMotionParams = RootMotionParams; // For animation root motion
const FRootMotionSourceGroup BackupRootMotion = CurrentRootMotion;
const bool bRealJump = CharacterOwner->bPressedJump;
const bool bRealCrouch = bWantsToCrouch;
const bool bRealForceMaxAccel = bForceMaxAccel;
CharacterOwner->bClientWasFalling = (MovementMode == MOVE_Falling);
CharacterOwner->bClientUpdating = true;
bForceNextFloorCheck = true;
// Replay moves that have not yet been acked.
// 回放本地已经执行的但是服务器还没确认的Move命令
UE_LOG(LogNetPlayerMovement, VeryVerbose, TEXT("ClientUpdatePositionAfterServerUpdate Replaying %d Moves, starting at Timestamp %f"), ClientData->SavedMoves.Num(), ClientData->SavedMoves[0]->TimeStamp);
for (int32 i=0; i<ClientData->SavedMoves.Num(); i++)
{
const FSavedMovePtr& CurrentMove = ClientData->SavedMoves[i];
CurrentMove->PrepMoveFor(CharacterOwner);
MoveAutonomous(CurrentMove->TimeStamp, CurrentMove->DeltaTime, CurrentMove->GetCompressedFlags(), CurrentMove->Acceleration);
CurrentMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Replay);
}
if (ClientData->PendingMove.IsValid())
{
ClientData->PendingMove->bForceNoCombine = true;
}
// Restore saved values.
AnalogInputModifier = SavedAnalogInputModifier;
RootMotionParams = BackupRootMotionParams;
CurrentRootMotion = BackupRootMotion;
if (CharacterOwner->bClientResimulateRootMotionSources)
{
// If we were resimulating root motion sources, it's because we had mismatched state
// with the server - we just resimulated our SavedMoves and now need to restore
// CurrentRootMotion with the latest "good state"
UE_LOG(LogRootMotion, VeryVerbose, TEXT("CurrentRootMotion getting updated after ServerUpdate replays: %s"), *CharacterOwner->GetName());
CurrentRootMotion.UpdateStateFrom(CharacterOwner->SavedRootMotion);
CharacterOwner->bClientResimulateRootMotionSources = false;
}
CharacterOwner->SavedRootMotion.Clear();
CharacterOwner->bClientResimulateRootMotion = false;
CharacterOwner->bClientUpdating = false;
CharacterOwner->bPressedJump = bRealJump;
bWantsToCrouch = bRealCrouch;
bForceMaxAccel = bRealForceMaxAccel;
bForceNextFloorCheck = true;
return (ClientData->SavedMoves.Num() > 0);
}
收到服务器的校正命令后,客户端会移除掉被服务器执行了的MoveData,然后回放列表中剩下的MoveData,这样本地玩家觉得自己的位置拉扯不大。
-
在客户端B上, Character_A_B的移动情况
Character_A_B在客户端B上的Role为ROLE_SimulatedProxy,所以执行的是SimulatedTick(DeltaTime)函数。此时Character_A_B上的位置、姿态等是服务器Replicate过来的;
/** Replicated movement data of our RootComponent.
* Struct used for efficient replication as velocity and location are generally replicated together (this saves a repindex)
* and velocity.Z is commonly zero (most position replications are for walking pawns).
*/
USTRUCT()
struct ENGINE_API FRepMovement
{
GENERATED_USTRUCT_BODY()
UPROPERTY(Transient)
FVector LinearVelocity;
UPROPERTY(Transient)
FVector AngularVelocity;
UPROPERTY(Transient)
FVector Location;
UPROPERTY(Transient)
FRotator Rotation;
/** If set, RootComponent should be sleeping. */
UPROPERTY(Transient)
uint8 bSimulatedPhysicSleep : 1;
/** If set, additional physic data (angular velocity) will be replicated. */
UPROPERTY(Transient)
uint8 bRepPhysics : 1;
/** Allows tuning the compression level for the replicated location vector. You should only need to change this from the default if you see visual artifacts. */
UPROPERTY(EditDefaultsOnly, Category=Replication, AdvancedDisplay)
EVectorQuantization LocationQuantizationLevel;
/** Allows tuning the compression level for the replicated velocity vectors. You should only need to change this from the default if you see visual artifacts. */
UPROPERTY(EditDefaultsOnly, Category=Replication, AdvancedDisplay)
EVectorQuantization VelocityQuantizationLevel;
/** Allows tuning the compression level for replicated rotation. You should only need to change this from the default if you see visual artifacts. */
UPROPERTY(EditDefaultsOnly, Category=Replication, AdvancedDisplay)
ERotatorQuantization RotationQuantizationLevel;
};
此时MovementMode也是服务器同步过来的:
小结
UE4中移动这块RPC都是不可靠的,所以存在用户命令丢失的情况,通过上述机制提升用户体验;这块内容比较复杂,细节很多,本文只阐述的大轮廓。在此推荐《multiplayer game programming》,该书对帧同步算法和CS模式的同步都有详细的阐述(本书作者之一参与过UE的开发)。该书中介绍的同步机制与UE的类似。
关于同步的时序图,可参考本人的博客一文CS模式网络游戏的运动同步总结。