UE4人物移動同步算法

UE4采用組件化設(shè)計思路,一個Actor上擁有多個Component, 這些Component可以是Mesh, Particles秋泳,Sound等其他功能性組件, 其中MovementComponent就是屬于功能性組件踱承,它對移動算法進行封裝倡缠,來控制Actor移動。

UMovementComponent

定義了基礎(chǔ)的移動輔助功能:

  • 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的碰撞不考慮载荔,直接設(shè)置到最終位置盾饮。
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這些事件是何時產(chǎn)生的,注意UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, OutHit, MoveComponentFlags, Teleport);懒熙,它會調(diào)用USceneComponent::MoveComponentImp接口丘损,因為UPrimitiveComponent具有物理碰撞數(shù)據(jù),所以我們直接瀏覽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每幀都會根據(jù)input vector被更新舆驶。同時提供網(wǎng)絡(luò)同步功能, 包含server-client校正和預(yù)測。

移動算法
在函數(shù)void UCharacterMovementComponent::PerformMovement(float DeltaSeconds)中實現(xiàn)移動功能而钞,

Paste_Image.png
Paste_Image.png

Server-Client移動同步

本節(jié)中分析Server-Client同步算法沙廉,移動包含如下幾點屬性:

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

UE的服務(wù)器的設(shè)計理念是一切以服務(wù)器為準,所有移動位置臼节、戰(zhàn)斗的判定都以服務(wù)器的結(jié)果為準撬陵。
考慮如下情形:現(xiàn)有服務(wù)器S, 客戶端A, 客戶端B,客戶端A上有個人物Character_A, 在服務(wù)器S上的鏡像為Character_A_S, 在客戶端B上的鏡像為Character_A_B网缝,關(guān)系圖示如下:

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 表示權(quán)威的意思(服務(wù)器才有)
  • ROLE_AutonomousProxy 表示具有自主操控功能(Client A上被玩家操控)
  • ROLE_SimulatedProxy 表示模擬(Client B上的Character_A_B是別的玩家操控的所以只模擬該人物移動)

下面對關(guān)鍵代碼進行分析注釋

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).
    // 如果此時使用物理引擎的模擬驅(qū)動人物移動(人物死后的布娃娃動畫)
    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, 需要處理服務(wù)器發(fā)來的移動校正信息
            ClientUpdatePositionAfterServerUpdate();
        }

        // Allow root motion to move characters that have no controller.
        if( CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()) )
        {  // 如果是本地玩家控制(在listen模式下,此時游戲既作為服務(wù)器又作為為客戶端)
            {
                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);  // 直接執(zhí)行PerformMovement 
            }
            else if (bIsClient)
            {
                ReplicateMoveToServer(DeltaTime, Acceleration); // 此時在客戶端上,玩家控制人物途凫,調(diào)用該函數(shù)處理移動請求垢夹。
            }
        }
        else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy) // 如果是在服務(wù)器端
        {
            // 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)

}

下面分類討論移動執(zhí)行:

  • 在客戶端A上, 操作Character_A
Paste_Image.png

此時執(zhí)行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;
    }

    // 分配客戶端預(yù)測數(shù)據(jù)結(jié)構(gòu)
    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);  //執(zhí)行本地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
        // 發(fā)起RPC調(diào)用维费,請求服務(wù)器執(zhí)行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;
    }
}

上面的客戶端執(zhí)行的流程為:

  1. 申請一個SavedMove項NewMove, 填寫當前的移動命令(加速度、Jump/Croud促王、Postion犀盟、Pose、MovementMode蝇狼、timeStamp等);
  2. 本地執(zhí)行PerformMovement();
  3. 將NewMove放入移動命令列表中;
  4. 發(fā)起遠程調(diào)用 CallServerMove()阅畴,請求服務(wù)器執(zhí)行移動命令
  • 在服務(wù)器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; // 客戶端的發(fā)送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);
        }

        // 執(zhí)行客戶端的請求
        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);

    // 服務(wù)器判斷客戶端是否出現(xiàn)大的執(zhí)行誤差
    ServerMoveHandleClientError(TimeStamp, DeltaTime, Accel, ClientLoc, ClientMovementBase, ClientBaseBoneName, ClientMovementMode);
}
// 服務(wù)器執(zhí)行客戶端的移動請求
void UCharacterMovementComponent::MoveAutonomous
    (
    float ClientTimeStamp,
    float DeltaTime,
    uint8 CompressedFlags,
    const FVector& NewAccel
    )
{
    if (!HasValidData())
    {
        return;
    }

    UpdateFromCompressedFlags(CompressedFlags); // 設(shè)置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);  // 執(zhí)行移動算法

    // 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;         // 設(shè)置標記,客戶端當前出現(xiàn)誤差
        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;   // 客戶端沒有出現(xiàn)誤差
    }

    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;
}

服務(wù)器會調(diào)用SendClientAdjustment()來通知客戶端NewMove的執(zhí)行結(jié)果

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收到服務(wù)器的移動反饋
    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()中有調(diào)用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) // 檢查收到校正通知時設(shè)置的標記
    {
        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.
    // 回放本地已經(jīng)執(zhí)行的但是服務(wù)器還沒確認的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);
}

收到服務(wù)器的校正命令后迅耘,客戶端會移除掉被服務(wù)器執(zhí)行了的MoveData贱枣,然后回放列表中剩下的MoveData,這樣本地玩家覺得自己的位置拉扯不大纽哥。

  • 在客戶端B上, Character_A_B的移動情況
    Character_A_B在客戶端B上的Role為ROLE_SimulatedProxy,所以執(zhí)行的是SimulatedTick(DeltaTime)函數(shù)栖秕。此時Character_A_B上的位置、姿態(tài)等是服務(wù)器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也是服務(wù)器同步過來的:

Paste_Image.png
Paste_Image.png
Paste_Image.png

小結(jié)

UE4中移動這塊RPC都是不可靠的暑塑,所以存在用戶命令丟失的情況吼句,通過上述機制提升用戶體驗;這塊內(nèi)容比較復(fù)雜事格,細節(jié)很多惕艳,本文只闡述的大輪廓搞隐。在此推薦《multiplayer game programming》,該書對幀同步算法和CS模式的同步都有詳細的闡述(本書作者之一參與過UE的開發(fā))尔艇。該書中介紹的同步機制與UE的類似尔许。
關(guān)于同步的時序圖,可參考本人的博客一文CS模式網(wǎng)絡(luò)游戲的運動同步總結(jié)终娃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末味廊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子棠耕,更是在濱河造成了極大的恐慌余佛,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窍荧,死亡現(xiàn)場離奇詭異辉巡,居然都是意外死亡,警方通過查閱死者的電腦和手機蕊退,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門郊楣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瓤荔,你說我怎么就攤上這事净蚤。” “怎么了输硝?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵今瀑,是天一觀的道長。 經(jīng)常有香客問我点把,道長橘荠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任郎逃,我火速辦了婚禮哥童,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衣厘。我一直安慰自己如蚜,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布影暴。 她就那樣靜靜地躺著错邦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪型宙。 梳的紋絲不亂的頭發(fā)上撬呢,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音妆兑,去河邊找鬼魂拦。 笑死毛仪,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的芯勘。 我是一名探鬼主播箱靴,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荷愕!你這毒婦竟也來了衡怀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤安疗,失蹤者是張志新(化名)和其女友劉穎抛杨,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荐类,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡怖现,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了玉罐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈嗤。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吊输,靈堂內(nèi)的尸體忽然破棺而出恢共,到底是詐尸還是另有隱情,我是刑警寧澤璧亚,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站脂信,受9級特大地震影響癣蟋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狰闪,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一疯搅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧埋泵,春花似錦幔欧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雁社,卻和暖如春浴井,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霉撵。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工磺浙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洪囤,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓撕氧,卻偏偏與公主長得像瘤缩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伦泥,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理剥啤,服務(wù)發(fā)現(xiàn),斷路器奄喂,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,110評論 25 707
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法铐殃,類相關(guān)的語法,內(nèi)部類的語法跨新,繼承相關(guān)的語法富腊,異常的語法,線程的語...
    子非魚_t_閱讀 31,630評論 18 399
  • _(:з」∠)_看不下去書做的手帳配件 1
    懿暖醬閱讀 484評論 2 1
  • 整個語感啟蒙系列的四張VCD我已經(jīng)去年陪女兒看過多次域帐,每首都有些印象赘被,只是沒有做到耳熟能詳?shù)牡夭健?Dance y...
    玩英語閱讀 232評論 0 0