学问很大啊,虚心逐步学习
内容来自虚幻引擎官方文档了解虚幻引擎角色移动组件中的网络移动 | 虚幻引擎 5.6 文档 | Epic Developer Community
角色移动基础知识
UCharacterMovementComponent
预先附加到ACharacter
Actor类以及根据它派生的所有 蓝图。在TickComponent运行期间,UCharacterMovementComponent 就会调用 PerformMovement,根据,根据当前使用的移动模式以及玩家输入的控制(APlayerController)计算世界中所需要的加速度,完成后,UCharacterMovementComponent会最终把速度应用给角色。
虽然ACharacter派生自APawn,但不仅仅是添加了移动组件的Pawn。这个UCharacterMovementComponent 和ACharacter 已经深度紧密配合了,是专门为角色移动设计的一套系统。ACharacter 甚至修改了复制相关的逻辑,以更好的支持UCharacterMovementComponent 工作,可以认为是深度定制的。
可以打开蓝图和CPP看到,确实有这个组件
在Cpp内,是一个指针组合过来的
下面先来看一下UCharacterMovementComponent
UCharacterMovementComponent
CharacterMovementComponent 用来处理关联Character所有者的移动逻辑,包括行走、跌倒、游泳、飞行、自定义。
移动是主要受到速度和加速度影响,根据受到的力,每一帧更新加速度。
同时实现了联网,包括服务器-客户端校正和预测。
看了下里面的具体实现,发现一堆继承和类。可以细化调节很多参数,包括阻力,最大速度加速度等等,这些还是等具体需要时候再看吧
网络期望同步
首先说三个概念 ROLE_Authority,ROLE_AutonomousProxy,ROLE_SimulatedProxy
- ROLE_Authority:就是服务器的权威。
- ROLE_AutonomousProxy:自主代理,玩家主控。该角色在客户端是拥有自治(Autonomous)权利的Character,如玩家控制的主角。这类移动一般是客户端接收到玩家输入本地模拟之后,再通过RPC发送给服务器进行模拟的
- ROLE_SimulatedProxy:模拟代理。当我们连接的时候,肯定不会控制全部角色,那对于不是本地客户端控制的其他角色或AI角色,就要通过服务器同步位置速度这些信息,然后给客户端进行模拟。
那我们期望是什么呢?主要一下几个方面
- Autonomous客户端无延迟:自己主动代理的客户端当然不可以有任何延迟,自己输入立马就要有输出。如果这时候有网络延迟,那对于玩家显然是不可以接受的
- 服务端位置是权威的:服务器上玩家位置必须是最权威的,包括你的Autonomous客户端。那这样就会对Autonomous客户端要求更高了,不仅需要实时响应玩家输入,同时也要和服务器同步
- Simulate客户端上移动表现平滑:你的角色在其他客户端表现也要尽可能的合理平滑,在Simulate客户端上肯定不能一直接收到位置更新信息,时不时还丢包之类的。因此需要适当的插值或者优化,反正需要保证移动平滑
- 反外挂:服务器对于不合理的移动请求给于拒绝,
- 移动同步流量优化:占用了大量带宽,怎么优化比较好。
下面这个图应该很好,可惜看不清,有机会再补一下
源码阅读
TickComponent
首先是 UCharacterMovementComponent::TickComponent ,用来做每帧更新角色移动逻辑,包括输入处理、物理模拟、网络同步、根运动处理等。
有三个参数,deltaTime时间间隔,TickType返回tick类型如 LevelTick、GameTick 等),ThisTickFunction指向当花钱的Tick函数指针,用于嵌套Tick调用管理。
能够分为10个模块去处理,下面大概讲一下这些部分
- 性能统计与输入处理准备
- 基础有效性检查
- 调用父类的 TickComponent
- 异步物理 Tick 分支处理
- 物理模拟中的角色处理
- 非物理模拟(正常角色)的移动逻辑
- 客户端预测与服务器同步
- 本地控制角色移动
- 基于其他对象的移动(如载具)
- 监听服务器上的代理平滑处理
- 模拟代理(Simulated Proxy)的处理
- RVO 避障逻辑
- 物理交互力应用
- 调试可视化
性能统计与输入处理准备
一开始看到的一堆宏就是,主要作用是标记和做性能分析的,不是考虑重点先忽略
SCOPED_NAMED_EVENT(UCharacterMovementComponent_TickComponent, FColor::Yellow);
SCOPE_CYCLE_COUNTER(STAT_CharacterMovement);
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementTick);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(CharacterMovement);
下面:
FVector InputVector = FVector::ZeroVector;
bool bUsingAsyncTick = (CharacterMovementCVars::AsyncCharacterMovement == 1) && IsAsyncCallbackRegistered();
if (!bUsingAsyncTick)
{
InputVector = ConsumeInputVector();
}
InputVector是玩家输入的向量,比如说通过WASD或者手柄输入,最后会存在这里。
接着判断下是否启用了异步物理Tick模拟,如果启用了,则先不处理,交给异步线程去处理。否则就直接消费掉输入,即ConsumeInputVector(),获取之前输入并清空
基础有效性检查
if (!HasValidData() || ShouldSkipUpdate(DeltaTime))
{
return;
}
检查角色和其UpdatedComponent 是否有效,以及判断下是否需要跳过这一帧。无效就返回
调用父类 TickComponent
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!HasValidData())
{
return;
}
调用父类的Tick,然后再检查是否合适,不合适就删了。
为什么要检查呢?因为父类的Tick可能已经修改了CharacterOwner
或者UpdatedComponent,这样导致组件被销毁,总之以防万一再检查一次。
异步物理 Tick 分支处理
if (bUsingAsyncTick)
{
...
return;
}
如果启用了异步物理 Tick:
- 获取角色骨骼网格体
USkeletalMeshComponent
。 - 调用
TickPose()
更新骨骼动画姿势。 - 处理根运动(Root Motion):
- 如果动画中有根运动,消费并累积到
RootMotionParams
中,用于后续移动。
- 如果动画中有根运动,消费并累积到
- 调用
AccumulateRootMotionForAsync()
处理异步环境下的根运动累积。
同时,在异步Tick中,不执行常规的角色移动逻辑,因为物理模拟和移动计算是在另一个线程中进行的。
物理模拟中的Actor处理
const bool bIsSimulatingPhysics = UpdatedComponent->IsSimulatingPhysics();
if (CharacterOwner->GetLocalRole() == ROLE_Authority && (!bCheatFlying || bIsSimulatingPhysics) && !CharacterOwner->CheckStillInWorld())
{
return;
}
if (bIsSimulatingPhysics)
{
...
return;
}
判断角色是否正在被物理引擎模拟,检查是否角色掉出世界之外了,那直接返回
如果角色正在模拟物理,客户端需要更新相机的位置以跟随物理移动
- 客户端可能需要更新相机位置以跟随物理移动。
- 清除累积的力(如推力等)。
- 直接返回,不执行常规移动逻辑。
这里不是重点,有些抽象。总之住部分一般是由物理引擎驱动的,不受玩家管理控制。
非物理模拟(正常角色)的移动逻辑
整个函数的核心部分,涉及客户端预测、服务器同步、根运动、动画驱动移动等复杂机制。
开始有一个
AvoidanceLockTimer -= DeltaTime;
这个是用于控制角色避障(Avoidance)行为的锁定时间,避免在同一帧或短时间内重复触发避障逻辑,提升性能和稳定性。
那为啥需要这个东西呢?
因为在多人游戏,你可能多个角色同时运动,如果没有合理的避让,就很容易出现角色扎堆穿模问题,为此,虚幻引擎提供了 RVO(Reciprocal Velocity Obstacles,互惠速度障碍)避障系统,让角色能够智能地避开其他移动中的角色或物体。
项目 | 说明 |
---|---|
变量名 | AvoidanceLockTimer |
类型 | float (时间值,单位通常是秒) |
作用 | 控制避障逻辑的触发频率,避免频繁计算,提升性能和稳定性 |
更新方式 | 每帧通过 AvoidanceLockTimer -= DeltaTime; 减少计时 |
典型用途 | 节流避障计算、锁定避障状态、与 RVO 系统协同工作 |
这个RVO呢,是一种基于速度空间的避障算法,在有多个玩家以某一个速度向一个方向移动时候,可能就会碰撞,这个RVO呢,就是用来让这些角色,重新设置一个互惠安全的新速度,使得双方可以都避开对方,同时还尽可能的接近自己原本目标的速度。
扯远了,继续回来
if (CharacterOwner->GetLocalRole() > ROLE_SimulatedProxy)
如果是服务器和本地角色,就会进入这个分支。
const bool bIsClient = (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client));
if (bIsClient)
{
FNetworkPredictionData_Client_Character* ClientData = GetPredictionData_Client_Character();
if (ClientData && ClientData->bUpdatePosition)
{
ClientUpdatePositionAfterServerUpdate();
}
}
前面的好理解,判断自己是否是主动代理,后面的IsNetMode(NM_Client)呢?这个就是来判断当前的网络模式的,比如说单机,还是专用服务器模式,监听服务器模式这些。
结合二者,前半部分判断是否是本地客户端控制的角色,后面说是是否在客户端上。因此综合下来,是一个自身控制的本地客户端角色
接着,下面获取角色相关的网络预测数据,如果预测数据中标记了需要更新数据,则调用ClientUpdatePositionAfterServerUpdate
。这个函数是根据服务器发送回来的最新数据,修正客户端的预测位置,如果实际表现不一致,就需要调用ClientUpdatePositionAfterServerUpdate来修正客户端状态,避免角色出现瞬移或者抖动这些状态。
即:在本地客户端上,根据服务器发送的位置更新信息,修正客户端的预测移动结果,确保角色移动的同步性和平滑性。
FScopedMeshMovementUpdate ScopedMeshUpdate(CharacterOwner->GetMesh());
延迟角色网格体(Skeletal Mesh)的更新,将多个移动操作合并为一次更新,提高性能并保证状态一致性
基于 RAII(资源获取即初始化)原则,构造函数标记延迟更新,析构函数执行实际更新
那这个是怎么做到的呢?稍微看了一下源码,好吧其实看不懂
struct FScopedMeshMovementUpdate
{
FScopedMeshMovementUpdate(USkeletalMeshComponent* Mesh, bool bEnabled = true)
: ScopedMoveUpdate(bEnabled && CharacterMovementCVars::bDeferCharacterMeshMovement ? Mesh : nullptr, EScopedUpdate::DeferredUpdates)
{
}
private:
FScopedMovementUpdate ScopedMoveUpdate;
};
这个接受两个参数,一个表示是表示需要被延迟更新的网格体组件,即角色的SkeletalMeshComponent,另一个是表示是否开启延迟更新功能,默认这个会打开。
后面还有一个参数,是EScopedUpdate::DeferredUpdates,这个意思是说,超出作用域之后,才会去执行前面的更新函数,那这延迟了个寂寞?不相当于还是一帧一次执行?
好家伙,看了下后面,他的延迟指的是:比如说我受到多个作用效果,包括什么物理效果,玩家输入,碰撞效果,动画驱动,网络同步后,预期期望是这些全部作用完之后,再更新我的mesh,把这些全部效果合并,最终得到我最终的网格体,从而减小开销。emmm原来还可以每个都单独执行吗(,开始还以为这些都是理所当然合并在一起的。
然后继续,
const bool bShouldPerformControlledCharMove = CharacterOwner->IsLocallyControlled()
|| (!CharacterOwner->Controller && bRunPhysicsWithNoController)
|| (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion());
这里,判断是否需要执行控制移动,如果满足一下任意一个条件就继续:
- 角色由本地玩家控制。
- 没有 Controller 但允许无 Controller 的物理移动。
- 没有 Controller 但正在播放根运动动画。
ControlledCharacterMove(InputVector, DeltaTime);
处理角色的输入,以驱动角色移动
在监听服务器上的代理平滑处理
- 如果是监听服务器上的自主代理(即本地玩家在服务器上控制角色):
- 调用
ServerAutonomousProxyTick()
处理服务器端的移动逻辑,确保与客户端预测一致。
- 调用
if (bIsaListenServerAutonomousProxy)
{
ServerAutonomousProxyTick(DeltaTime);
}
接着,服务器开始处理远程客户端代理
else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)
{
MaybeUpdateBasedMovement(DeltaTime);
MaybeSaveBaseLocation();
ServerAutonomousProxyTick(DeltaTime);
// 平滑处理
if (CharacterMovementCVars::NetEnableListenServerSmoothing && !bNetworkSmoothingComplete && IsNetMode(NM_ListenServer))
{
SmoothClientPosition(DeltaTime);
}
}
- 处理服务器上由远程客户端控制的角色代理:
MaybeUpdateBasedMovement()
:如果角色是基于其他对象移动(如载具),更新其位置。MaybeSaveBaseLocation()
:保存基础位置用于后续平滑。ServerAutonomousProxyTick()
:执行服务器端的移动逻辑。- 如果启用了网络平滑,调用
SmoothClientPosition()
平滑角色位置,减少网络延迟带来的抖动。
模拟代理(Simulated Proxy)的处理
else if (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy)
{
FScopedMeshMovementUpdate ScopedMeshUpdate(CharacterOwner->GetMesh());
if (bShrinkProxyCapsule)
{
AdjustProxyCapsuleSize();
}
SimulatedTick(DeltaTime);
}
- 对于远程客户端看到的角色(非本地控制):
- 延迟网格体更新。
- 如果需要缩小胶囊体(优化碰撞检测),调用
AdjustProxyCapsuleSize()
。 - 调用
SimulatedTick()
执行模拟代理的移动逻辑,通常是基于服务器发送的位置数据进行插值或预测。
RVO 避障逻辑
if (bUseRVOAvoidance)
{
UpdateDefaultAvoidance();
}
如果启动了就更新避障行为,之前有过介绍具体内容
物理交互力应用
if (bEnablePhysicsInteraction)
{
ApplyDownwardForce(DeltaTime);
ApplyRepulsionForce(DeltaTime);
}
如果启动了物理交互:
ApplyDownwardForce()
:施加向下的力(如重力补充或踩踏效果)。ApplyRepulsionForce()
:施加排斥力(如击退效果)。
调试可视化
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if (CharacterMovementCVars::VisualizeMovement > 0)
{
VisualizeMovement();
}
#endif
这是核心函数,最终所有角色移动相关的功能最终都会在这里实现。
内容参考
了解虚幻引擎角色移动组件中的网络移动 | 虚幻引擎 5.6 文档 | Epic Developer Community
(76 封私信 / 52 条消息) UE4/UE5 Character Movement Component移动组件网络同步详解 – 知乎