0. 写在前面
本文为个人学习的笔记整理,如有错误,望不吝指出。
1. 概述
UE4的移动及同步代码主体在UCharacterMovementComponent
本篇将分为客户端本地移动流程、服务端简要移动流程、以及移动同步三块进行描述。
一些重要的枚举
- 移动类型:ENetRole
– ROLE_SimulatedProxy:模拟代理,即由服务端控制的本地角色,或者第三方玩家
– ROLE_AutonomousProxy:自主代理,主客户端
– ROLE_Authority:权威,服务端用,如NPC,怪物 - 模拟的插值类型:ENetworkSmoothingMode
– Disabled
– Linear:线性插值
– Exponential:指数插值
– Replay - 当前游戏跑的网络模式:ENetMode
– NM_Standalone:单机客户端
– NM_DedicatedServer:服务端模式
– NM_ListenServer:服务端模式
– NM_Client:联网客户端模式 - RootMotion模式:ERootMotionMode
– NoRootMotionExtraction
– IgnoreRootMotion:提取RootMotion数据但不控制位移
– RootMotionFromEverything:所有的RootMotion动画都会被应用且控制位移
– RootMotionFromMontagesOnly:只有Montage下的RootMotion会被应用且控制位移
只有RootMotionMontage适合进行网络同步
2. 客户端本地移动流程
本段描述客户端本地的移动流程,主要描述AutonomousProxy(自主代理)以及SimulatedProxy(模拟代理)的本地移动流程。
其中UCharacterMovementComponent::TickComponent是移动组件每帧迭代的入口,其根据移动类型(ENetRole)、网络模式(ENetMode)进到不同的计算逻辑。
2.1 SimulatedProxy移动流程
1.SimulatedTick:当本地角色类型(ACharacter->LocalRole)是模拟代理时,由TickComponent调用。其中逻辑主要分为4块
- NetworkSmoothingMode == ENetworkSmoothingMode::Replay:和回放相关,暂略
- CharacterOwner->IsPlayingNetworkedRootMotionMontage():RootMotionMontage同步,主要流程为TickCharacterPose、SimulateRootMotion、SimulatedRootMotionPositionFixup。在后面同步篇章详细描述。
- CurrentRootMotion.HasActiveRootMotionSources:
- 最后是常规的模拟代理的移动同步,服务端同步速度、旋转、时间戳,客户端根据这些属性进行移动模拟。根据是否包含RootMotion动画(区别RootMotionMontage),分为两个模拟流程:SimulateMovement和PerformMovement。
下面详细描述模拟代理的最后一个部分
2.SimulateMovement:
- FScopedMovementUpdate
- bNetworkUpdateReceived:该帧是否有收到网络更新的数据,进一步检查,是否需要切换MoveMode,或者是否有瞬移或强制检查地面的标识,需要调FindFloor重新检查地面。ApplyNetworkMovementMode/UpdateFloorFromAdjustment
- UpdateBasedMovement
- MoveSmooth:在没有及时收到服务器数据时尝试以当前的速度方向进行预测,判断当前是否在地面上移动,是的话调MoveAlongFloor尝试移动。否则调SafeMoveUpdatedComponent进行一系列的物理检测后移动
- 预测后如果是还在地面移动,需要尝试寻找新的地面信息。如果是掉落(Fall)则需要处理掉落速度。
- SaveBaseLocation、UpdateComponentVelocity
- 最后调SmoothClientPosition,胶囊体先行,Mesh平滑插值。其中SmoothClientPosition_Interpolate负责插值数据,SmoothClientPosition_UpdateVisuals负责更新Mesh
3.PerformMovement:基于当前速度或者当前RootMotion数据提取的速度,进行一系列的移动模拟
- FScopedMovementUpdate
- StartNewPhysics:关键函数,根据MoveMode进行不同的物理计算。有Walking、NavWalking、Falling、Flying、Swimming等几种模式,跑各自的物理逻辑,允许在一个tick内,当移动模式发生变化时,重新进行新模式的计算,但是有个迭代次数上限MaxSimulationIterations
2.2 PhysWalking移动逻辑
StartNewPhysics中贴地移动的计算
1.PhysWalking:
- 切时间片计算,为的是在比TickTime更小的时间间隔内计算移动逻辑。否则对细节要求比较高的情况来说,可能出现TickTime间隔过大的问题。
- 切时间片:
timeTick = GetSimulationTimeStep(remainingTime, Iterations);
remainingTime -= timeTick;
当remainingTime过小或者迭代次数过多时(达到MaxSimulationIterations)退出
该函数具体的计算逻辑如下:
- MaintainHorizontalGroundVelocity:在上下坡时,是维持水平分量速度,还是将完整的速度映射到斜面
- CalcVelocity(timeTick, GroundFriction, false, GetMaxBrakingDeceleration()):没有RootMotion控制时,基于当前移动模式更新速度和加速度,应用摩擦力以及加速减速效果,但不应用重力。
- ApplyRootMotionToVelocity:尝试应用RootMotion数据到速度,有可能会导致Falling,如果导致Falling则重新跑StartNewPhysics
- MoveAlongFloor:更新完速度后,尝试移动,做一系列的碰撞检测
- FindFloor:通过物理(胶囊体)尝试寻找地面
2.MoveAlongFloor(MoveVelocity, timeTick, &StepDownResult):
- ComputeGroundMovementDelta:处理斜坡时的速度方向,将水平速度投影到斜坡上,计算一个平行于斜坡的速度
- 调SafeMoveUpdatedComponent进行Sweep检测
- 根据碰撞结果Hit.bStartPenetrating,解决渗透问题:SlideAlongSurface
- 如果碰撞,对碰撞点做可行走面的检查,如果是可行走面,则计算新的移动方向,重新跑SafeMoveUpdatedComponent
- 如果碰撞,试着延着障碍物移动:SlideAlongSurface
MoveAlongFloor结束后,移动状态可能会改变,判断Falling和Swimming(是否掉落和落水),如果有改变的话重新跑StartNewPhysics
3.SafeMoveUpdatedComponent:UPrimitiveComponent::MoveComponentImpl
-
碰撞检测:从TraceStart(Vec3)到TraceEnd(Vec3)进行Sweep检测
MyWorld->ComponentSweepMulti(Hits, this, TraceStart, TraceEnd, InitialRotationQuat, Params)
=>FPhysicsInterface::GeomSweepMulti,返回TArray,碰撞结果列表,存到Hits中
=>对Hits中的每个碰撞结果执行PullBackHit:所有Hit需要拉回微小的距离(缩小hit.time),避免因为浮点数精度的问题导致跟碰撞物重叠
=>如果Hits中有多个碰撞结果,选择切面法线和移动方向最相反的碰撞结果 -
坐标设置:
如果第一步检测中没有碰撞,则Location设为TraceEnd
如果发生了碰撞,根据比例Hit.Time和检测方向,算一个挨墙的位置
更新坐标:InternalSetWorldLocationAndRotation(NewLocation, NewRotationQuat, bSkipPhysicsMove, Teleport)
=>NewLocation、NewRotationQuat:碰撞检测后算出来的新位置和旋转
=>SetRelativeLocation_Direct、SetRelativeRotation_Direct,根据位置和旋转是否有变化,设置数据
=>UpdateComponentToWorldWithParent:更新ComponentToWorld Transform矩阵(TODO) -
更新重叠的状态:UpdateOverlap
-
返回检测结果Hits
4.StepUp:爬台阶的逻辑
- 三次调用MoveUpdatedComponent(最后调的UPrimitiveComponent::MoveComponentImpl,和SafeMoveUpdatedComponent一样),尝试先向上移动,再向前移动,最后向下移动。
- 其中每次移动计算结束,都会检查碰撞、渗透、掉落等一系列逻辑。
- 向上如果有碰撞,直接退出StepUp,如果有渗透,执行ScopedStepUpMovement.RevertMove()回退这次移动。
向前如果有碰撞,执行SlideAlongSurface尝试延墙移动
向下如果有渗透,也回退。如果是碰撞,还要检查是否可行走面、边缘判断等,也要进行RevertMove
5.FindFloor:
- 垂直向下Sweep查找地面,将地面信息整合后向外输出FFindFloorResult
6.SlideAlongSurface:
- 原先的移动方向会碰墙,改变移动方向为贴墙走
新的移动方向计算:SlideDelta = ComputeSlideVector(Delta, Time, Normal, Hit)
用新的移动方向跑SafeMoveUpdatedComponent
如果碰墙(移动方向有两面墙)则需要进行TwoWallAdjust,计算在两面墙下的新的移动方向(移动方向和两面墙的夹角相关)
7.TwoWallAdjust
8.ComputeGroundMovementDelta
2.4 AutonomousProxy移动流程
1.ReplicateMoveToServer:
主客户端在TickComponent时,通过该接口调用PerformMovement进行本地移动,并且记录移动数据,提交到服务端
- 检查PendingMove是否有值,和NewMove是否可以合并
- PerformMovement,执行客户端移动
- 将NewMove加入SavedMoves,检查是否符合立即上报的条件,如果不符合,则先缓存到PendingMoves,下次再发送。如果符合,则调用CallServerMove将移动数据发送给服务端。
自主代理的移动主要也是依赖于PerformMovement,其中PendingMove、SavedMoves是和同步相关的变量,在后面详细描述。
3. 服务端简要流程
4. 客户端同步流程
4.1 SimulatedProxy同步
1.值复制:
属性Acharacter::ReplicatedBasedMovement被服务器修改时,会回调OnRep_ReplicatedBasedMovement进行处理
2.OnRep_ReplicatedBasedMovement:
- IsPlayingNetworkedRootMotionMontage时直接退出不处理,走OnRep_RootMotion接口
- 赋值BasedMovement = ReplicatedBasedMovement
- 计算新的目标位置和旋转
- 服务端传相对差值:
NewLocation = CharacterMovement->OldBaseLocation + ReplicatedBasedMovement.Location
NewRotation = (FRotationMatrix(ReplicatedBasedMovement.Rotation) * FQuatRotationMatrix(CharacterMovement->OldBaseQuat)).Rotator(); - 绝对值:
NewRotation = ReplicatedBasedMovement.Rotation;
- 服务端传相对差值:
- 做位置纠正:
CharacterMovement->SmoothCorrection(OldLocation, OldRotation, NewLocation, NewRotation.Quaternion());- 计算ClientData(FNetworkPredictionData_Client_Character)里的位置offset(MeshTranslationOffset)和旋转offset(MeshRotationOffset),后面在SmoothClientPosition进行Mesh插值时会用到。
- 时间相关处理:
SmoothingClientTimeStamp、SmoothingServerTimeStamp、LastCorrectionDelta
也是在SmoothClientPosition中用到,SmoothingClientTimeStamp和SmoothingServerTimeStamp用来处理内插还是外插
LastCorrectionDelta用于在内插时计算比例
4.2 RootMotionMontage同步
1.RepRootMotion和OnRep_RootMotion:
- Acharacter->RepRootMotion:
FRepRootMotionMontage的结构体对象,双端进行蒙太奇同步的属性,主要包含蒙太奇轨道数据,位置和旋转数据等。 - FSimulatedRootMotionReplicatedMove:
结构体,其中包含一个时间戳(客户端收到蒙太奇同步数据时的本地时间)和一个FRepRootMotionMontage对象 - OnRep_RootMotion:
蒙太奇同步属性修改时的回调函数,只有角色类型为模拟代理(SimulatedProxy)时,将本次数据加入到一个同步数据列表(RootMotionRepMoves)的末尾。 - Acharacter- >RootMotionRepMoves:FSimulatedRootMotionReplicatedMove数据列表
2.SimulatedTick中Montage同步流程
- TickCharacterPose:更新动画姿态,并且尝试提取RootMotion数据到位移组件中
- 调SkeletalMeshComp->TickPose进行姿态更新,提取RootMotion数据到动画系统
- 调SkeletalMeshComp->ConsumeRootMotion,返回一个FRootMotionMovementParams数据对象,其中存着更新动画后,动画系统提取的RootMotion的数据,并且根据角色设置的RootMotionScale(缩放数据),调整上述的RootMotion数据
- SimulateRootMotion:
- 将第一步提取的局部坐标下的RootMotion运动数据转换到世界坐标
- 根据世界坐标下的RootMotion数据计算当前速度,并调用StartNewPhysics进行移动模拟
- 注意,这里在StartNewPhysics中只会处理位移,在移动结束后才会继续调MoveUpdatedComponent处理旋转
- SimulatedRootMotionPositionFixup:
4.3 AutonomousProxy同步
1.服务端的SendClientAdjustment通过RPC告诉客户端是否需要矫正,或者确认某次移动有效性。
如果需要矫正,客户端RPC接口都会更新ClientPredictionData 的bUpdatePosition 为True,在下一次TickComponent时会执行矫正。
2.FNetworkPredictionData_Client_Character:客户端预测数据Comp->ClientPredictionData
负责记录移动情况和处理来自服务器的矫正,在客户端执行本地移动、准备要发送到服务器的移动,以及处理矫正时,其参数将被频繁引用和更改。
- 主要数据:
1)各种时间戳
2)各种移动数据列表:SavedMoves、FreeMoves、PendingMove、LastAckedMove - TArray SavedMoves:将客户端每次执行移动的关键数据保存下来,并且后续会上报给服务端,当服务端确认过时间戳的移动数据合法后,将该数据及之前的数据从SavedMoves移除。如果服务端需要矫正客户端移动,则会从SavedMoves中最早未被确认过的数据开始重播。
- FSavedMovePtr PendingMove:
1)移动系统网络带宽的一种优化,当一次NewMove结束后会判断是否立即发送给服务器(网络速度问题、发送间隔短等)。如果不立即发送,则会赋值给PendingMove,在客户端下一次Tick后,判断两次移动是否可以合并,如果可以,则只发送一次数据,如果不行,则连发两次数据(PendingMove和NewMove)
2)PendingMove->CanCombineWith(NewMove),是否可以合并
3)NewMove->CombineWith(PendingMove),执行合并
4)ServerMoveDual、ServerMoveDualHybridRootMotion:一次发送两个数据 - TArray FreeMoves:
- FSavedMovePtr LastAckedMove:
4.ClientUpdatePositionAfterServerUpdate:
- 在处理玩家输入之前,需要先调用该函数,检查服务器是否下发了纠正数据需要处理,有的话则优先处理
对SavedMoves中未被服务端确认的数据,从头开始加速重播
重播结束后,才执行当帧本该执行的移动逻辑:ReplicateMoveToServer
5. 参考资料
https://zhuanlan.zhihu.com/p/33529865
https://zhuanlan.zhihu.com/p/34257208
https://docs.unrealengine.com/4.27/zh-CN/InteractiveExperiences/Networking/CharacterMovementComponent/