1390 字
7 分钟
Character Movement——UE 最被低估的子系统

一、CMC 管的事比你想象的多#

UCharacterMovementComponent(CMC)不是简单的”把 Pawn 从 A 移到 B”。它同时管着:

CMC 的职责:
├── 移动模式管理(Walking / Falling / Swimming / Flying / Custom)
├── 物理约束(重力、摩擦力、最大速度、加速度)
├── 碰撞检测(Sweep——移动路径上有没有障碍物)
├── 步高处理(Step Up——遇到小台阶自动踏上而不是撞停)
├── 网络预测(客户端预测移动 + 服务端校正)
├── Root Motion 消费(如果角色在用 Root Motion 驱动移动)
├── 移动同步(ServerMove RPC 的发送与接收)
└── 斜坡/楼梯处理(Walkable Slope Angle——多陡的坡能走)

CMC 的存在是 UE 里几乎所有”角色移动奇怪 bug”的根因或中转站。


二、移动模式(Movement Mode)#

CMC 定义了几种内置的移动模式:

MOVE_Walking → 在地面上行走(Gravity 生效,Step Up 生效)
MOVE_Falling → 在空中(Gravity 加速,直到落地或死亡平面)
MOVE_Swimming → 在水中(Buoyancy 浮力 + 水阻力)
MOVE_Flying → 在空气中飞行(无重力,作弊/载具模式)
MOVE_Custom → 自定义(爬墙、滑索、载具——你写逻辑接管)

模式之间的切换:

Walking → Falling:地面检测不到 → IsFalling
Falling → Walking:碰撞检测到地面 → Landed
Walking → Swimming:进入水体 Volume → StartSwimming

CMC 每帧做的事(简化)

void UCharacterMovementComponent::TickComponent(float DeltaTime) {
// 1. 根据当前模式计算加速度
FVector Accel = CalcVelocity(DeltaTime);
// 2. 应用加速度 → 计算新速度
Velocity = Velocity + Accel * DeltaTime;
// 3. 执行移动——Sweep(碰撞检测)
FVector NewPosition = OldPosition + Velocity * DeltaTime;
FHitResult Hit;
SafeMoveUpdatedComponent(NewPosition, UpdatedComponent->GetComponentQuat(), true, Hit);
// 4. 处理碰撞结果
if (Hit.bBlockingHit) {
HandleImpact(Hit); // 撞墙了——反弹或停止
SlideAlongSurface(...); // 沿墙滑动
}
// 5. 更新地面状态
UpdateFloor(); // 脚下有地面吗 → 决定 Walking/Falling
}

三、网络层:ServerMove 的完整调用链#

这是 CMC 最复杂也最重要的部分。和第 7 篇(网络模型)讲过的预测/和解完全对应,这里是 UE 的具体实现。

1 完整数据流#

[客户端]
1. 玩家按 W → 本地 TickComponent → 立即移动角色(Client Prediction)
2. 构造 FSavedMove_Character(存这一帧的输入 + Timestamp + 位置)
3. 调用 ServerMove RPC:
ServerMove(Timestamp, Acceleration, ClientPackedPosition, ...)
→ 这是 Unreliable RPC——丢了也没关系,下一帧会重发
[服务端]
4. 收到 ServerMove
5. ServerMove_Implementation:
→ 用客户端传来的 Acceleration 模拟移动(服务端也有 CMC)
→ 得到服务端权威位置
→ 比较客户端位置 vs 服务端权威位置
→ 如果差距 < 阈值 → 接受客户端预测(不发校正)
→ 如果差距 > 阈值 → 发送 ClientAdjustment RPC(校正!)
[客户端]
6. 收到 ClientAdjustment:
→ 把角色拉到服务端权威位置
→ 重放(Replay)SavedMoves 中所有未被确认的移动

2 FSavedMove#

客户端每帧保存一次移动快照:

class FSavedMove_Character {
FVector StartLocation; // 这帧开始时的位置
FVector SavedLocation; // 预测后的位置
FVector Acceleration; // 输入加速度
FRotator SavedRotation; // 朝向
float Timestamp; // 时间戳(用于服务端匹配)
uint8 CompressedMoveFlags; // 压缩的标志位(WantsToJump、IsSprinting...)
};
// 客户端维护一个 SavedMoves 队列
TArray<FSavedMove_Character> SavedMoves;

当收到校正时:找到第一个未确认的 SavedMove,从校正后的位置开始,按顺序重放后续所有 SavedMove——每一帧的运动重新模拟一遍。这就是第 7 篇说过的”服务器和解”。

3 为什么 CMC 用了 Unreliable RPC#

ServerMove 是 Unreliable——丢了不重传。为什么?

  • 每帧都发包(60fps = 每秒 60 个 ServerMove RPC)
  • 丢一个完全没关系——下一个 ServerMove 里已经包含了累计的移动信息
  • 可靠 RPC 的确认+重传机制对 60Hz 的移动同步来说是浪费

四、CMC 的常见问题与调优#

问题原因解决方案
角色在斜坡上”抖动”地面检测反复切换 Walking ↔ Falling增大 Walking.StepHeight 或调 PerchRadiusThreshold
高延迟下角色瞬移校正差距太大调大 ClientSidePredictionTolerance,或降低 NetUpdateFrequency
”卡墙角”时无法移动Sweep 碰撞后的 Slide 计算有问题检查 MaxStepHeight 和碰撞体形状
Root Motion 动画播放中角色不移动CMC 没有启用 Root MotionCMC->bAllowPhysicsRotationDuringAnimRootMotion = true
网络客户端移动”滑”Client Prediction 和 Server 物理模拟不一致导致持续微小校正统一客户端/服务端的 MaxWalkSpeedGravityScale

五、CMC 的替代方案——什么时候不用它#

CMC 是为**类人角色(Humanoid Character)**设计的——走路、跑步、跳跃、游泳。如果你的游戏角色不需要这些:

场景替代方案
载具(车、飞机)UChaosVehicleMovementComponent
飞行射击不用 CMC——直接用 FloatingPawnMovement 或手写
固定轨道移动(电梯、过山车)UInterpToMovementComponent
第一人称解谜或观光游戏很可能完全不需要 CMC——Pawn 不动,只移动相机

六、总结#

概念一句话
移动模式Walking/Falling/Swimming/Flying——CMC 根据物理状态自动切换
ServerMove客户端的 Unreliable RPC——每帧发送输入,丢了也没关系
FSavedMove客户端保存的移动快照——用于收到校正后的重放
ClientAdjustment服务端告诉客户端”你预测错了,拉回来+重放未确认输入”
校正阈值客户端和服务端位置差距多大才触发校正——太小浪费带宽,太大角色瞬移

CMC 是 UE 最精妙的子系统之一——它在客户端预测和服务端权威之间做出了一个极其精密的平衡。理解了 CMC 的网络同步,你就理解了 UE 多人游戏里”角色为什么瞬移”的全部答案。

Character Movement——UE 最被低估的子系统
https://www.m4doka.xyz/posts/ue/ue-4-character-movement/
作者
m4doka
发布于
2026-08-08
许可协议
CC BY-NC-SA 4.0