UE原生MovementBase实现分析

这是一个非常重要但容易被忽视的函数。它的职责是:处理"角色站在一个移动的物体上"的场景 ------ 让角色跟随电梯、船、旋转平台、公交车等一起移动,而不是像踩空一样被平台甩下。

什么是 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) { ... }
}
相关推荐
天人合一peng3 小时前
Unity工程发布hololens需安装, MRTK安装
unity·游戏引擎·hololens
weixin_409383124 小时前
godot 调用class方法得用实例 不能用脚本引用
游戏引擎·godot
风酥糖4 小时前
Godot游戏练习01-第32节-国际化
游戏·游戏引擎·godot
魔士于安4 小时前
Unity类似博物馆场景
前端·unity·游戏引擎·贴图·模型
RReality5 小时前
【Unity Shader URP】模板遮罩 / 传送门 实战教程
ui·unity·游戏引擎·图形渲染·材质
晴夏。5 小时前
UE原生第三人称相机源码分析
游戏·ue5·ue4·相机·ue·3c
郑寿昌13 小时前
虚幻引擎6:Lumen光源技术前瞻
游戏引擎·虚幻
RPGMZ1 天前
RPGMakerMZ 获取敌人攻击时属性 用于画UI或属性克制
javascript·游戏引擎·rpgmz·rpgmakermz
zdr尽职尽责1 天前
Untiy 处理Aseprite 资产 解决偏移问题
学习·unity·c#·游戏引擎