UE4人物移动同步算法

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中。

Paste_Image.png
Paste_Image.png
Paste_Image.png

UCharacterMovementComponent

CharacterMovementComponent处理了Character对象的移动逻辑,支持多种移动模式:walking, falling, swimming, flying, custom。移动主要受当前的velocity和acceleration影响。acceleration每帧都会根据input vector被更新。同时提供网络同步功能, 包含server-client校正和预测。

移动算法
在函数void UCharacterMovementComponent::PerformMovement(float DeltaSeconds)中实现移动功能,

Paste_Image.png
Paste_Image.png

Server-Client移动同步

本节中分析Server-Client同步算法,移动包含如下几点属性:

  • 移动位置速度同步
  • MoveMode同步(jump, walking, ...)

UE的服务器的设计理念是一切以服务器为准,所有移动位置、战斗的判定都以服务器的结果为准。
考虑如下情形:现有服务器S, 客户端A, 客户端B,客户端A上有个人物Character_A, 在服务器S上的镜像为Character_A_S, 在客户端B上的镜像为Character_A_B,关系图示如下:

UE4_Character_Movement_01.jpg

当前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
Paste_Image.png

此时执行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;
    }
}

上面的客户端执行的流程为:

  1. 申请一个SavedMove项NewMove, 填写当前的移动命令(加速度、Jump/Croud、Postion、Pose、MovementMode、timeStamp等);
  2. 本地执行PerformMovement();
  3. 将NewMove放入移动命令列表中;
  4. 发起远程调用 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收到服务器的移动反馈
    1. 收到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);
}
  1. 收到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过来的;
    Paste_Image.png
/** 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也是服务器同步过来的:

Paste_Image.png
Paste_Image.png
Paste_Image.png

小结

UE4中移动这块RPC都是不可靠的,所以存在用户命令丢失的情况,通过上述机制提升用户体验;这块内容比较复杂,细节很多,本文只阐述的大轮廓。在此推荐《multiplayer game programming》,该书对帧同步算法和CS模式的同步都有详细的阐述(本书作者之一参与过UE的开发)。该书中介绍的同步机制与UE的类似。
关于同步的时序图,可参考本人的博客一文CS模式网络游戏的运动同步总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,806评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • _(:з」∠)_看不下去书做的手帐配件 1
    懿暖酱阅读 481评论 2 1
  • 整个语感启蒙系列的四张VCD我已经去年陪女儿看过多次,每首都有些印象,只是没有做到耳熟能详的地步。 Dance y...
    玩英语阅读 225评论 0 0