这是一个非常重要但容易被忽视的函数。它的职责是:处理"角色站在一个移动的物体上"的场景 ------ 让角色跟随电梯、船、旋转平台、公交车等一起移动,而不是像踩空一样被平台甩下。
什么是 MovementBase(基座)?
CMC 中有一个 MovementBase 指针,代表角色当前"站"在什么物体上:
| 角色状态 | MovementBase |
|---|---|
| 站在普通地面上 | 地面的 StaticMeshComponent |
| 站在电梯上 | 电梯的 Mesh 组件 |
| 跳到空中 | nullptr(空中没有基座) |
| 站在另一个角色头顶上 | 那个角色的 CapsuleComponent |
问题:没有这个函数会怎样?
假设有一部电梯以 100 cm/s 向上移动,角色站在上面不做任何输入:
时间 t=0: 电梯 Z=0, 角色 Z=100(站在电梯顶)
时间 t=1s: 电梯 Z=100, 角色 Z=100 ❌ 角色悬浮在空中,电梯"穿过"了角色
因为 CMC 的物理只算角色自身的 Velocity 和输入,它不知道脚下的电梯在动。
核心机制总结:如何让角色"黏"在移动平台上?
用一句话本质概括整个机制:
每帧对比"脚下平台"的新旧变换,把变化量复刻到角色身上。
整个过程浓缩成三步核心逻辑:
① 记住你脚下是什么
当角色落在任何可站立的物体上时(普通地面、电梯、船、另一个角色...),CMC 会在地面检测时把它记录为 MovementBase,并保存一份当前的变换:
OldBaseLocation = 基座当前位置 // 例:电梯 Z=0
OldBaseQuat = 基座当前旋转 // 例:旋转平台 Yaw=0°
② 每帧对比"脚下的平台动没动"
下一帧 UpdateBasedMovement 获取基座现在的位置和旋转,与上一帧的记录对比。如果 Old ≠ New,说明平台动了,就要"带着角色一起动"。
③ 把平台的位置变化"复刻"到角色上 (最关键)
简单点处理,就是
DeltaPos = NewBaseLocation - OldBaseLocation 然后平移角色
但是这样会带来一个问题,就是无法处理平台旋转的问题。
核心问题:!!#ff0000 平台不只会"平移",还会"旋转"!!
简单相减 只能处理一种情况------平台整体平移 (比如电梯上下走)。
但只要平台转个身,简单相减就彻底失灵了。为什么?
用一个生活场景就能理解
想象你站在一个巨大的旋转餐桌边缘(就是那种中餐馆里转菜的大圆盘),餐桌在原地顺时针转:
俯视图:
你 🧍
↓
╔═══════════╗
║ ║
║ · ║ ← 餐桌中心(从未移动过)
║ ║
╚═══════════╝
餐桌中心的世界坐标:(0, 0, 0)
从来没动过!
但是,如果餐桌原地转了90度,但是其位置是没变的,那结果会导致角色也不变。
所以想要实现真正平台带着角色转动,需要另外的方法。
双重变换的思路:把角色当成平台的一部分
UE 的做法换了个视角:不看"平台整体动了多少",而是看"角色应该跟着平台贴到哪里去"。
接下来讲实现思路
┌──────────────────────────────────────────────────┐
│ │
│ 想象你把角色用胶水"粘"在餐桌的那个点上。 │
│ │
│ 第1步:记住角色贴在餐桌的**哪个点**上。 │
│ 比如"餐桌中心往东 5 米的边缘位置"。 │
│ │
│ 第2步:餐桌转完后,那个点**跑到哪里了**? │
│ 转 90° 后,"餐桌中心往东 5 米"变成了 │
│ "餐桌中心往北 5 米"。 │
│ │
│ 第3步:把角色放到那个新位置去。 │
│ │
└──────────────────────────────────────────────────┘
这就是"世界→局部→世界"的三步走:
,而是通过 "世界→局部→世界" 双重变换:
先算:角色在"旧基座"坐标系里的相对位置
LocalPos = OldBaseTransform⁻¹ × 角色世界位置
再算:用"新基座"Transform 还原到世界
NewWorldPos = NewBaseTransform × LocalPos
得到:角色应该到的新位置,然后带碰撞检测地移动过去
数学符号预备知识:Transform 和 ⁻¹ 到底是什么?
别被数学符号吓到,它们背后的思想非常直观。用"贴纸和木板"的比喻就能理解。
Transform(变换)是什么?
Transform 不是一个神秘的数学概念,它就是一个**"位置 + 旋转 + 缩放"**的打包信息:
OldBaseTransform = {
Location: (10, 20, 0), // 平台在世界的哪里
Rotation: Yaw=45°, // 平台朝哪个方向转了
Scale: (1, 1, 1) // 平台多大(通常是1)
}
一个 Transform 描述的就是:"这个物体在世界里的姿态"。
Transform × 某个位置 是干嘛的?(没有 ⁻¹ 的版本)
想象你有一张小贴纸贴在一块木板上:
木板
╔══════════╗
║ ║
║ 🎯 ║ ← 贴在这里(相对木板的"局部位置")
║ ║
╚══════════╝
如果把整块木板搬到桌子的别处,还转了 90°,那张贴纸在桌面上的实际位置也会跟着变。
Transform × 局部位置 就是在问:
"这张贴纸贴在木板的这个点上,木板现在这样摆着,那贴纸在世界里实际到哪里去了?"
用一句话:把"相对物体的位置"换算成"在世界中的位置"。
Transform⁻¹ × 某个位置 是干嘛的?(带 ⁻¹ 的版本)
⁻¹ 叫**"逆变换"**,它干的事情正好相反。
继续贴纸的例子------这次反过来问:
"我看到桌子上有个 🎯 点,它相当于贴在木板的哪个位置?"
Transform⁻¹ × 世界位置 就是在做这个"反向换算":把"在世界中的位置"换算回"相对物体的位置"。
两者合起来:穿衣 / 脱衣
把两个操作想象成穿衣服 / 脱衣服:
┌────────────────────────────────────────────────────────┐
│ │
│ 局部坐标 世界坐标 │
│ "相对木板" ◄──── 脱衣 ⁻¹ ──── "桌面上" │
│ (贴纸角度) ───── 穿衣 ────► (实际位置) │
│ │
│ Transform × 局部 = 世界 (穿上平台的姿态) │
│ Transform⁻¹ × 世界 = 局部 (脱掉平台的姿态) │
│ │
└────────────────────────────────────────────────────────┘
一个穿、一个脱,互为逆操作。UE 的双重变换就是"先脱掉旧平台的衣服(得到贴的位置),再穿上新平台的衣服(得到跟平台走的新位置)"。
旋转到底是怎么算的?用数字走一遍完整流程
这是很多人最困惑的地方------"旋转"听起来玄学,但用具体数字推一遍就清楚了。
场景设定:
俯视图(Z轴朝上,看平台的鸟瞰图):
+Y (北)
↑
│
────┼──── +X (东)
│
│
初始状态:
- 旋转平台中心在世界原点 (0, 0, 0)
- 角色站在平台"东边5米"的位置 → 世界坐标 (5, 0, 0)
- 平台 Yaw = 0°(没转)
现在平台原地逆时针转了 90°:
- 平台中心仍在 (0, 0, 0)(没移动)
- 平台 Yaw = 90°
问题: 角色应该被带到哪里去?凭直觉应该是平台"北边5米",即 (0, 5, 0)。下面看数学是怎么算出这个结果的。
第一步:脱衣 ------ 算角色贴在平台的哪个点
OldBaseTransform = { Loc=(0,0,0), Yaw=0° }
角色世界位置 = (5, 0, 0)
LocalPos = OldBaseTransform⁻¹ × (5, 0, 0)
= 先减去平台位置,再反向旋转
步骤1:减去平台位置
(5, 0, 0) - (0, 0, 0) = (5, 0, 0)
步骤2:反向旋转(平台没转,所以不变)
旋转 -0° → (5, 0, 0)
→ LocalPos = (5, 0, 0)
含义:"角色贴在平台的东边5米处"
第二步:平台原地转 90°(状态变化)
NewBaseTransform = { Loc=(0,0,0), Yaw=90° }
第三步:穿衣 ------ 算那个局部点现在在世界哪里
这一步是旋转计算的核心,我们详细展开。
NewWorldPos = NewBaseTransform × LocalPos(5, 0, 0)
= 先旋转 90°,再加上平台位置
步骤1:旋转 90°(Yaw 的旋转 = 绕 Z 轴旋转)
Yaw 旋转 90° 的具体算法:
在 UE 中,Yaw 的旋转是绕 Z 轴旋转 (俯视图逆时针为正)。把一个点 (x, y, z) 绕 Z 轴旋转角度 θ 的公式是:
新x = 旧x × cos(θ) - 旧y × sin(θ)
新y = 旧x × sin(θ) + 旧y × cos(θ)
新z = 旧z (Z 不变,因为绕 Z 轴转)
代入 θ = 90°,(x, y, z) = (5, 0, 0):
cos(90°) = 0
sin(90°) = 1
新x = 5 × 0 - 0 × 1 = 0
新y = 5 × 1 + 0 × 0 = 5
新z = 0
→ 旋转后 = (0, 5, 0) ← 局部"东5米"变成了世界"北5米"
继续步骤2:
步骤2:加上平台位置
(0, 5, 0) + (0, 0, 0) = (0, 5, 0)
→ NewWorldPos = (0, 5, 0) ✓
含义:"角色跟着平台转到了世界的北5米位置"
第四步:得出位移差
DeltaPosition = NewWorldPos - 角色旧世界位置
= (0, 5, 0) - (5, 0, 0)
= (-5, 5, 0)
含义:"角色需要往西5米 + 往北5米 才能跟上平台"
(这就是沿圆周移动 90° 的位移)
完整的旋转算例可视化:
旋转前 旋转后
+Y ↑ +Y ↑
│ │
│ 🧍 · │ ← 角色到这 (0, 5, 0)
│ │
·────┼────· 🧍 → (5, 0, 0) ·────┼────·
│ 角色在东5米 │
│ │
世界坐标 平台转90°后
角色沿圆周移到北5米
关键洞察:
- 旋转不是把向量"变长变短",而是改变它的方向
- 向量
(5, 0, 0)长度是 5;旋转 90° 后变成(0, 5, 0),长度仍然是 5 - 只是"东边5米"变成了"北边5米"------方向变了,距离没变
- 这正是刚体旋转的本质:物体绕中心转,每个点都沿同一圆周移动
如果平台同时"平移 + 旋转",会怎么算?
把上面的例子再复杂一点:平台不只转 90°,还向东平移了 10 米。
OldBaseTransform = { Loc=(0,0,0), Yaw=0° }
NewBaseTransform = { Loc=(10,0,0), Yaw=90° } ← 同时平移+旋转
角色旧世界位置 = (5, 0, 0)
计算过程:
脱衣:LocalPos = (5, 0, 0) - (0,0,0) 再反转0° = (5, 0, 0)
穿衣:NewWorldPos = 先转90°:(5,0,0) → (0, 5, 0)
再加平移:(0,5,0) + (10,0,0) = (10, 5, 0)
位移:DeltaPosition = (10, 5, 0) - (5, 0, 0) = (5, 5, 0)
角色需要往东5米 + 往北5米
(既跟上平台的平移,又跟上平台的旋转)
看到没?双重变换一次性把"平移"和"旋转"都处理好了,不需要写两套代码。 这就是 UE 为什么选它作为万能解。
回头看 CMC 源码就好理解了
cpp
// 第一步:脱掉"旧平台"的 Transform
FVector LocalBasePos = OldLocalToWorld.InverseTransformPosition(角色世界位置);
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// 这就是 OldBaseTransform⁻¹ × 世界位置
// 人话:角色相当于贴在旧平台的哪个点上?
// 第二步:穿上"新平台"的 Transform
FVector NewWorldPos = NewLocalToWorld.TransformPosition(LocalBasePos);
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// 这就是 NewBaseTransform × 局部位置
// 人话:那个贴的点,在新平台姿态下跑到世界哪里了?
每一行对应一个"脱衣/穿衣"动作,整个算法就是**"把角色当成贴纸,跟着平台一起做刚体运动"**。
除了位移,还同步了什么?
如果基座转动了,还会同步三样东西,缺一不可:
- ✅ 角色身体朝向:跟着平台一起转(否则身体相对平台会反向扭)
- ✅ Controller 朝向(相机) :通过
UpdateBasedRotation更新,否则玩家视角会"被甩出"平台
完整实现代码如下:
cpp
void UCharacterMovementComponent::UpdateBasedMovement(float DeltaSeconds)
{
// 1. 基本检查:基座存在且有效?
const UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase();
if (!IsValid(MovementBase)) return;
// 2. 获取基座的新旧变换
FVector NewBaseLocation;
FQuat NewBaseQuat;
MovementBaseUtility::GetMovementBaseTransform(
MovementBase, BoneName, NewBaseLocation, NewBaseQuat);
// 3. 计算基座的变化量
const bool bRotationChanged = !OldBaseQuat.Equals(NewBaseQuat);
FQuat DeltaQuat = NewBaseQuat * OldBaseQuat.Inverse();
// 4. ⭐ 关键:通过"局部坐标"重算角色应该到的新位置
// (这是处理旋转平台的关键,不是简单的位置相减)
const FQuatRotationTranslationMatrix OldLocalToWorld(OldBaseQuat, OldBaseLocation);
const FQuatRotationTranslationMatrix NewLocalToWorld(NewBaseQuat, NewBaseLocation);
FVector BaseOffset(0, 0, HalfHeight); // 基于胶囊底部而非中心
FVector LocalBasePos = OldLocalToWorld.InverseTransformPosition(
UpdatedComponent->GetComponentLocation() - BaseOffset);
FVector NewWorldPos = NewLocalToWorld.TransformPosition(LocalBasePos) + BaseOffset;
DeltaPosition = NewWorldPos - UpdatedComponent->GetComponentLocation();
// 5. 应用位移(带碰撞检测)
MoveUpdatedComponent(DeltaPosition, FinalQuat, true, &MoveOnBaseHit);
// 6. 如果基座旋转了,同步更新角色朝向 + Controller朝向(让相机跟转)
if (bRotationChanged && !bIgnoreBaseRotation) { ... }
}