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:地面检测不到 → IsFallingFalling → Walking:碰撞检测到地面 → LandedWalking → Swimming:进入水体 Volume → StartSwimmingCMC 每帧做的事(简化):
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. 收到 ServerMove5. 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 Motion | CMC->bAllowPhysicsRotationDuringAnimRootMotion = true |
| 网络客户端移动”滑” | Client Prediction 和 Server 物理模拟不一致导致持续微小校正 | 统一客户端/服务端的 MaxWalkSpeed、GravityScale |
五、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/