CharacterMovement源码浅析

Base

classDiagram

	direction LR
	
	class UMovementComponent {
		UpdatedComponent : TObjectPtr~USceneComponent~ (处理空间位置)
		UpdatedPrimitive : TObjectPtr~UPrimitiveComponent~ (处理渲染物理)
	}

	%% -------------
	
	UMovementComponent<|--UProjectileMovementComponent
	class UProjectileMovementComponent {
		支持发射体(子弹等)
	}
	
	%% -------------
	
	UMovementComponent<|--UNavMovementComponent
	class UNavMovementComponent {
		支持 Agent 寻路
		NavAgentProps : FNavAgentProperties
	}
	
	UNavMovementComponent<|--UPawnMovementComponent
	class UPawnMovementComponent {
		支持输入控制
		AddInputVector()
	}
	
	UPawnMovementComponent<|--UCharacterMovementComponent

Move 一般是先进行基础运动(PerformMovent),然后处理基于物理的模拟(CollisionSimulation);

flowchart LR
UCharacterMovementComponent

TickComponent
-->ConsumeInputVector

TickComponent
-->ControlledCharacterMove

TickComponent
-->Other...
  1. ComsumeInputVector

PawnOwner 取出累积的 ControlInputVector,该值监听输入并调用 Pawn::AddMovementInput 得来;

  1. ControlledCharacterMove

进行 Character 移动的输入处理、物理模拟、同步;

  1. Other...

ControlledCharacterMove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds)
{
{
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(DeltaSeconds);

// apply input to acceleration
Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));
AnalogInputModifier = ComputeAnalogInputModifier();
}

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

ControlledCharacterMove
-->CheckJumpInput
-.->ScaleInputAcceleration
-.->ComputeAnalogInputModifier
ControlledCharacterMove
-->|ROLE_Authority|PerformMovement
-->Start(StartNewPhysics)
ControlledCharacterMove
-->|ROLE_AutonomousProxy & IsClient|ReplicateMoveToServer

Start-->MOVE_None
Start-->MOVE_Walking
Start-->MOVE_Falling
Start-->MOVE_Flying
Start-->MOVE_Swimming
Start-->MOVE_Custom

Input

解析输入相关的数据;

  1. CheckJumpInput:根据 bPressedJump,计算 JumpCurrentCountJumpForceTimeRemaining
  2. ScaleInputAcceleration:根据玩家的输入 InputVector,计算出当前的初始加速度值;
  3. ComputeAnalogInputModifier:模拟输入修正值,将 Acceleration /= MaxAcceleration,限制在 0-1 内;

PerfomeMovement

进行基础运动模拟,设置位移。

Kinetic : Walking

flowchart LR
UCharacterMovementComponent

%%-------------------------------------


PerformMovement
-->StartNewPhysics
-->PhysWalking


%%-------------------------------------

PhysWalking
-->GetSimulationTimeStep
PhysWalking
-->CalcVelocity
PhysWalking
-->MoveAlongFloor
PhysWalking
-->FindFloor
PhysWalking
-->CheckLedges
PhysWalking
-->MaintainHorizontalGroundVelocity

%%-------------------------------------

MoveAlongFloor
-->ComputeGroundMovementDelta
MoveAlongFloor
-->SafeMoveUpdatedComponent

%%-------------------------------------
CheckLedges
-->|true|GetLedgeMove

GetLedgeMove
-->|true|RevertMove
-->TryLedgeMove
GetLedgeMove
-->|false|bMustJump
-->|true|RevertMove
-->Fall


CheckLedges
-->|false|FloorCheck
%%-------------------------------------

FloorCheck
-->IsWalkableFloor

IsWalkableFloor
-->|true|AdjustFloorHeight
-->SetBase

IsWalkableFloor
-->|false|GetPenetrationAdjustment
-->ResolvePenetration


%%-------------------------------------

GetSimulationTimeStep:将 TickDeltaTime 按照 MaxSimulationTimeStep 分割为若干段(为了保证平滑),处理每一段的信息;

CalcVelocity:根据 FrictionbFluidBrakingDeceleration 修改 Acceleration,并计算出 Velocity 水平速度;

MoveAlongFloor:根据 MoveVelocity 信息,先调用 ComputeGroundMovementDelta,根据 Velocity 计算出 RampVector 平行于斜面的移动距离,同时根据 bMaintainHorizontalGroundVelocity 处理沿斜面速度减慢的情况;然后调用 SafeMoveUpdatedComponent,进行 MoveUpdatedComponent,更新位置;在 UPrimitiveComponent::MoveComponentImpl 中,还会进行 World->ComponentSweepMulti 判断是否遇到障碍物;

FindFloor:更新 CurrentFloor : FFindFloorResult 信息;用 CharacterOwner->GetCapsuleComponent() 进行 FloorSweepTest,计算出 ValidPerchRadius 等信息;

CheckLedges:检测是否在 Ledge 附近,如果是,则先尝试寻找新的移动方向(通过 GetLedgeMove 进行 SweepSingleByChannel 计算出边缘法线返回新的反向);如果找不到新的方向,则尝试跳跃,检测 bMustJump,如果不能跳跃则取消移动;

FloorCheck:校验 Floor 相关数据,如果 Character 处于IsWalkableFloorFloorAdjustFloorHeight 来调整 Character 的高度,如果在 Floor 中则 GetPenetrationAdjustment 计算需要弹出 Character 的距离并 ResolvePenetration,防止 FloorCharacter 有冲突卡住;

MaintainHorizontalGroundVelocity:调用之前判断是否依然 IsMovingOnGround,如果是则根据 bMaintainHorizontalGroundVelocity,计算 GravityRelativeVelocity 进而更新 Velocity

Kinetic : Falling

flowchart LR

PerformMovement
-->StartNewPhysics
-->PhysFalling

PhysFalling
-->GetFallingLateralAcceleration

PhysFalling
-->ShouldLimitAirControl

PhysFalling
-->RestorePreAdditiveRootMotionVelocity

PhysFalling
-->CalcVelocity

PhysFalling
-->ApplyRootMotionToVelocity

PhysFalling
-->NotifyJumpApex

PhysFalling
-->SafeMoveUpdatedComponent

PhysFalling
-->|IsSwimming|StartSwimming

PhysFalling
-->BlockingHit

%%---------------------

BlockingHit
-->|IsValidLandingPoint|ProcessLanded

BlockingHit
-->HandleImpact


%%---------------------

ProcessLanded
-->|IsFalling|SetPostLandedPhysics

ProcessLanded
-->StartNewPhysics_2
%%---------------------

HandleImpact
-.-> CalcVelocity_2
-.-> BlockingHit_2

GetFallingLateralAcceleration:计算 Character 在水平方向上的加速度;重点是将 WorldAcceleration 通过RotateWorldToGravity 转为 Gravity 相关坐标系,然后将 Z 的方向设为 0,再转回 World 坐标系,这样以移除垂直方向上的加速度(因为垂直方向的加速度需要由 Gravity 决定,而不是 InputVector);

RestorePreAdditiveRootMotionVelocityApply AdditiveRootMotion 的情况下将 Velocity 设置为 LastPreAdditiveVelocityAdditiveRootMotion 表示 RootMotioinVelocity 将与 Character 的原始速度 LastPreAdditiveVelocity,即计算 RootMotionVelocity 前的速度叠加),防止 RootMotion Velocity 被累加;

CalcVelocity:根据 FallAccelerationGravityJumpForce 等数据,计算出 NewFallVelocity

ApplyRootMotionToVelocity:应用 RootMotion Velocity,根据 HasOverrideVelocity / HasAdditiveVelocity 两种应用速度方式,计算 Velocity

NotifyJumpApex:当 RotateWorldToGravity(Velocity).Z < 0 时,说明到达了 JumpApex 跳跃顶点,进行通知;

SafeMoveUpdatedComponent:进行位移设置;

BlokingHit:在碰到障碍物时的处理;

ProcessLanded:判断是否 IsValidLandingPoint,如果是,进行着陆;进行通知 Landed 与设置相关物理状态 SetPostLandedPhysics,然后开始新的物理模拟 StartNewPhysics

HandleImpact:无法着陆时,AddImpactPhysicsForces ,用于后续计算碰撞后的 Velocity与位移;

BlockingHit_2:碰撞移动后再次计算是否再次 BlockingHit,如果无 Hit,则尝试 FindFloor,找到 Floor则尝试着陆;如果是,说明 Character 被卡在了两个障碍物中间;检测是否 IsValidLandingPoint 是着陆点,是则 ProcessLand;如果不是着陆点,特殊处理被卡住 bDitch 的情况(检查 OldHitImpactNormalHit.ImpactNormal 是否都具有 Z 即斜坡朝上,且夹角 >90° 即斜坡朝向不同,同时 CharacterDelta.Z 接近 0 即在垂直方向无移动),如果是,尝试增加 Velocity 与位移,摆脱被卡住的情况;

Kinetic : Other

TODO…

ReplicateMoveToServer

对于 AutonomousProxy Character 将移动同步到服务器,同时进行 Client 本地的预表现;

首先需要了解:FNetworkPredictionData

FNetworkPredictionData

PredictionData_Client_Character 维护 ClientMove 相关数据,同时用于合并、丢弃、比较、标记更新等操作;

classDiagram
	class FNetworkPredictionData_Client_Character {
		SavedMoves : TArray~FSavedMovePtr~
		FreeMoves : TArray~FSavedMovePtr~
		PendingMove : FSavedMovePtr
		LastAckedMove : FSavedMovePtr
		ClientUpdateRealTime : float
		bUpdatePosition : uint32
		...
	}
	
	FNetworkPredictionData_Client_Character-->FSavedMove_Character
	
	class FSavedMove_Character {
        TimeStamp : float
        DeltaTime : float
		Acceleration : FVector
		MaxSpeed : float
		Start / End / Saved : Location / ReletiveLocation / Rotation / Velocity / Floor 
		/ CapsuleRadius / CapsuleHalfHeight / Base / ActorOverlapCounter ...
		...
	}

其中:

FSavedMovePtrTSharedPtr<FSavedMove_Character>

SavedMoves 保存 Client 执行的 Move,在 CleintAck 后, LastAckedMove 将会被 Free 并从 SavedMoves 中移除;

PendingMove 记录 Client 最新执行的,还未 CallServerMove,每次 PushSavedMoves 中,同时可能会作为 OldMoveCombine

FreeMoves 记录已经被标记 FreeMove,后续释放;

classDiagram
    class FNetworkPredictionData_Server_Character {
    	PendingAdjustment : FClientAdjustment
	}
	
	FNetworkPredictionData_Server_Character-->FClientAdjustment
	class FClientAdjustment {
		TimeStamp : float
		DeltaTime : float
		bAckGoodMove : bool
		New Loc / Vel / Rot / Base ...
		...
	}

PredictionData_Server_Character 记录在 Server 上的 Move 数据,用于校验、修正等;

其中:

PendingAdjustment 维护了一系列 ClientAdjust 所需的数据;

ReplicateMoveToServer - Logic

flowchart LR

ReplicateMoveToServer
-->GetPredictionData_Client_Character

ReplicateMoveToServer
-->ClientData_UpdateTimeStampAndDeltaTime

ReplicateMoveToServer
-->FindImportantMove

ReplicateMoveToServer
-->ClientData_CreateSavedMove
-.->Move_SetMoveFor
-.->Move_Combine

ReplicateMoveToServer
-->PerformMovement
-.->Move_PostUpdate
-.->ClientData_SaveMove

ReplicateMoveToServer
-->CallServerMove
-.->ClearPending

GetPredictionData_Client_Character:获取 Client 的预测数据 ClientData : FNetworkPredictionData_Client_Character*

ClientData 会在各个地方被更新,比如

  1. ReplicateMoveToServer 中:
    更新物理模拟的 TimeStampDeltaTimeClientData->UpdateTimeStampAndDeltaTime
    创建新的 SavedMoveClientData->CreateSavedMove()
    PerformMovement 之后更新 LocationRotationVelocity 等数据:NewMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Record);
  2. CallServerMove / CallServerMovePacked 前更新时间:ClientData->ClientUpdateRealTime = MyWorld->GetRealTimeSeconds();
  3. ClientAckGoodMoveClient 收到 Server 的移动确认时,更新最后的移动 ClientData->LastAckedMove

FindImportantMove:找到最早的未 AckImportantMove 数据,IsImportantMove 指与上一个 Ack 的移动有差异的移动;判定是否 Important 时,会检查 CompressedFlags (压缩了 FLAG_JumpPressedFLAG_WantsToCrouch 等信息)、Start/End PackedMovementModeAcceleration 的大小、方向差异是否超过阈值;找到 Unack ImportMove 后,存储在 OldMove 中,后续将其与新的 Move 一起 CallServerMove,确保 Server 可以正确处理。

CreateSavedMove:创建新的 FSavedMove 数据,也就是定义一个新的 Move

Move_SetMoveFor:根据 CharacterOwnerDeltaTimeNewAcceleration 等数据设置 Move 基本信息;

Move_Combine:尝试将这个新的 Move 与 待处理的移动 PendingMove 合并,如果 CanCombine,更新 RotationPosition 等信息;CanCombine 会校验 TimeStampRootMotionAccelerationStartVelocityMaxSpeedJumpCompressedFlagsMovementModeStartCapsule Radius/HalfHeightAttachParentTimeDilationActorOverlapCounter 这些数据;Combine 时更新 LocationRotationVelocityFloorJump 等数据;

PerformMovement:在本地执行移动;

Move_PostUpdate:在 PerformMovement 更新了移动相关数据之后,设置这些状态数据到 Move 中;

ClientData_SaveMove:将 NewMove 保存到移动列表 ClientData->SavedMoves 中;

CallServerMove:根据角色是否正在复制移动 bSendServerMove 将新的移动发动到 Server,根据 ShouldUsePackedMovementRPCs 决定发送的方式 CallServerMovePacked / CallServerMove

ClearPendingMove:清空 PendingMove,表示没有待处理的移动;

AutonomousProxy

ReplicateMoveToServer -> CallServerMovePacked 继续出发:

flowchart LR

CallServerMovePacked
-->ServerMovePacked_ClientSend
-->|DS|ServerMovePacked_Implementation
-->ServerMovePacked_ServerReceive

ServerMovePacked_ServerReceive
-->SetCurrentNetworkMoveData
ServerMovePacked_ServerReceive
-->ServerMove_PerformMovement

ServerMove_PerformMovement
-->MoveAutonomous
ServerMove_PerformMovement
-->ServerMoveHandleClientError
-->ServerCheckClientError

MoveAutonomous
-->PerformMovement

通过 CallServerMovedPacked (UnreliableRPC) 将打包的 SaveMoves 数据发送到 DSDS 根据 Client 发送到的数据进行 MoveAutonomouss 与 校验数据合法性 CheckClientError,如果数据差异过大,则 ServerData->PendingAdjustment.bAckGoodMove = false

flowchart LR

UNetDriver::TickFlush
-->UNetDriver::ServerReplicateActors
-->SendClientAdjustment

SendClientAdjustment
-->bAckGoodMove

bAckGoodMove
-->|true|ServerLastClientGoodMoveAckTime
-->ShouldUsePackedMovementRPCs_Good
bAckGoodMove
-->|false|ServerLastClientAdjustmentTime
-->ShouldUsePackedMovementRPCs_NoGood

ShouldUsePackedMovementRPCs_Good
-->|false|ClientAckGoodMove
ShouldUsePackedMovementRPCs_NoGood
-->|false|ClientAdjustPosition

ShouldUsePackedMovementRPCs_Good
-->|true|ServerSendMoveResponse
ShouldUsePackedMovementRPCs_NoGood
-->|true|ServerSendMoveResponse

ServerSendMoveResponse
-->MoveResponsePacked_ServerSend
-->ClientMoveResponsePacked
-->|Client|MoveResponsePacked_ClientReceive

同步时候向 Client 进行 SendClientAdjust,通知 Client 每次 NewMove 的结果;

根据 ShouldUsePackedMovemtnRPCs 决定是否需要 ServerSendMoveResponse

flowchart LR

MoveResponsePacked_ClientReceive
-->ClientHandleMoveResponse
-->IsGoodMove

IsGoodMove
-->|true|ClientAckGoodMove_Implementation

IsGoodMove
-->|false|ClientAdjustPosition_Implementation
-->SetbUpdatePosition_true

TickComponent
-->ClientUpdatePositionAfterServerUpdate

Client 收到 DSSendClientAdjust 后,判定 MoveResponse 是否 IsGoodMove,如果是,Client 进行 Ack,被确认的 Move 将会立刻从 SavedMoves 中移除;否则 Client 需要更新 bUpdatePositiontrue,后续在 ClientUpdatePositionAfterServerUpdate 中进行修正;

ClientUpdatePositionAfterServerUpdate:判定 bUpdatePosition 是否是 true,如果是则回放 DSAckClientData->SavedMoves.Num(),进行 SetCurrentReplayedSavedMoveMoveFor

SimulateProxy

flowchart LR

ACharacter::OnRep_ReplicatedMovement
-->AActor::PostNetReceiveVelocity

ACharacter::OnRep_ReplicatedMovement
-->ACharacter::PostNetReceiveLocationAndRotation


AActor::PostNetReceiveVelocity
-->UPrimitiveComponent::SetPhysicsLinearVelocity


ACharacter::PostNetReceiveLocationAndRotation
-->SmoothCorrection
ACharacter::PostNetReceiveLocationAndRotation
-->SetbNetworkUpdateReceived_true
flowchart LR

TickComponent
-->SimulatedTick
-->SimulateMovement

SimulatedTick
-->|!bNetworkSmoothingComplete|SmoothClientPosition

%% -----------

SimulateMovement
-->ScopedUpdates


ScopedUpdates
-->bIsSimulatedProxy

bIsSimulatedProxy
-->bNetworkUpdateReceived_true

bNetworkUpdateReceived_true
-->|bNetworkGravityDirectionChanged|SetGravityDirection
bNetworkUpdateReceived_true
-->|bNetworkMovementModeChanged| ApplyNetworkMovementMode
bNetworkUpdateReceived_true
-->|bJustTeleported OR bForceNextFloorCheck|UpdateFloorFromAdjustment

bNetworkUpdateReceived_false
-->|bForceNextFloorCheck|UpdateFloorFromAdjustment

%% -----------

ScopedUpdates
-->UpdateCharacterStateBeforeMovement
ScopedUpdates
-->MaybeUpdateBasedMovement
ScopedUpdates
-->UpdateProxyAcceleration

ScopedUpdates
-->|!bHandledNetUpdate OR !bNetworkSkipProxyPredictionOnNetUpdate|MoveSmooth
-->IsMovingOnGround
IsMovingOnGround
-->|true|MoveAlongFloor
IsMovingOnGround
-->|false|SafeMoveUpdatedComponent
-->|!bSteppedUp|SlideAlongSurface


ScopedUpdates
-->UpdateCharacterStateAfterMovement
ScopedUpdates
-->OnMovementUpdated
SimulateMovement
-->CallMovementUpdateDelegate
SimulateMovement
-->UpdateComponentVelocity

%% -----------
SmoothClientPosition
-->SmoothClientPosition_Interpolate
SmoothClientPosition
-->SmoothClientPosition_UpdateVisuals

Smooth

SmoothingServerTimeStamp 表示 CharacterDS 当前移动时间戳,由 ACharacter::PreReplication 时,同步的 ReplicatedServerLastTransformUpdateTimeStamp 得来;

SmoothingClientTimeStamp 表示 Character 在这个 Client 当前平滑到的移动时间戳;

每次进行 SmoothClientPosition_Interpolate 时;

SmoothingMode = ENetworkSmoothingMode::Linear 的情况下:

  1. 计算 TargetDelta = LastCorrectionDelta,这里的 LastCorrectionDelta = 上一次( SmoothingServerTimeStamp - SmoothingClientTimeStamp) ,表示实际上相比于 DS 上的数据,Client 在这一次平滑开始前,还剩余多少时间还未执行平滑操作;
  2. 更新 SmoothingClientTimeStamp = Min(SmoothingClientTimeStamp + DeltaSeconds, SmoothingServerTimeStamp + MaxTimeAhead);
    这里的 DeltaSeconds 表示当帧过去的实际时间;
    MaxTimeAhead = TargetDelta * 0.15f,表示允许多往前外插的时间,0.15f 是允许多预测的时间比例;
    现在这个新的 SmoothingClientTimeStamp,就表示这一帧 Client 需要平滑到的时间戳;
  3. 计算 RemainingTime = SmoothingServerTimeStamp - SmoothingClientTimeStamp, 表示在这一帧平滑过后,还剩下多少时间没有平滑;然后 CurrentSmoothTime = TargetDelta - RemainingTime,得到这一帧需要平滑多少时间;
  4. 计算 LerpPercent = FMath::Clamp(CurrentSmoothTime / TargetDelta, 0.0f, LerpLimit),按照 本次平滑多少时间 / 剩余的总共需要平滑的时间,得到这个 LerpPercent
    其中 LerpLimit = 1.15f,也是允许多平滑的比例;
  5. 得到 LerpPercent 后,更新 MeshTranslationOffsetMeshRotationOffset

参考

UE 5.4 源码

大体框架:UE4 移动的网络同步

核心代码:《Exploring in UE4》移动组件详解[原理分析]