Unity里让一个物体跟随另一个物体大多数人第一反应是“直接改Transform.position就行”。日常原型这么写确实没问题但一旦到了复杂项目——队伍阵型、动态加载的场景、需要保持相对偏移的挂点、或者要在多套坐标空间之间来回切换——直接赋值会暴露出各种隐藏问题。矩阵方案不是炫技而是用Matrix4x4把位置、旋转、缩放统一封装成一条完整的“空间变换链”复制矩阵就等于复制了目标的世界姿态这也是引擎底层实际工作的方式。这篇文章会把矩阵跟随从原理讲到代码再讲到我实测中踩过的坑。适合正在做RPG队形、载具挂点、武器装备点、以及所有“物体跟着另一个动但不完全一样动”场景的Unity开发者刚入门矩阵的朋友也能通过跟随这个例子把矩阵从“听说过”变成真正会用。1. 为什么用矩阵而不是直接改Transform1.1 直接赋值方案的三个现实问题最直觉的跟随写法谁都会写void Update() { transform.position target.position; transform.rotation target.rotation; }这个小Demo能跑但仔细想有三个问题。第一它丢失了“相对偏移”。如果你希望物体跟随的是目标前方一米、偏左三十度的一个挂点位置直接赋值做不到还需要自己算偏移而偏移一旦涉及旋转就麻烦。第二它把“世界坐标”和“本地坐标”混在一起一旦目标位于某个嵌套的子物体层级中target.position是它的世界坐标但你的物体可能处于另一个父物体下直接赋值会导致父节点叠加的变换全部失效。第三它完全没有空间隔离的思路想复用这段逻辑到不同场景几乎只能复制粘贴。如果在Update里直接使用Transform层级——把跟随物设为目标的子物体——也能解决部分问题但子物体方案在运行时动态改父级容易引发坐标跳变而且会破坏原有继承缩放尤其在UIBatch合并、动态加载场景里会出现连锁问题。矩阵方案正是为了规避这些麻烦。1.2 矩阵方案的核心思路在哪里Unity里的Matrix4x4本质上就是“一个坐标系相对于另一个坐标系的完整描述”。它把旋转、平移、缩放全部压缩在4x4的数字网格里并且可以用矩阵乘法来叠加任意数量的坐标变换。跟随的核心思路很简单每帧提取目标的世界矩阵localToWorldMatrix把它乘上你自己的相对偏移矩阵得到你自身应当处的位置姿态然后把这个矩阵拆回Transform字段。因为矩阵同时携带了旋转和位移所以自然就能保持相对偏移、继承旋转、同步缩放。从另一个角度看矩阵方案相当于把“跟随”从“每帧读几个数字”变成了“每帧做一次空间状态的整体复制”。这不仅更接近Unity内部渲染的真实情况更在逻辑上统一了“目标世界姿态 - 相对偏移 - 自身世界姿态”的链路方便后续把同一套逻辑复用到任意物体上。2. 吃透Matrix4x4再动手代码只是最后一步2.1 矩阵里那16个数字到底代表什么Matrix4x4有4行4列共16个数字但你在实际使用时不需要记住全部。最核心的是区分“位移部分”和“旋转缩放的3x3部分”。从localToWorldMatrix来看最后一列m03, m13, m23就是这个物体在世界空间中的位置。左上角的3x3子矩阵则保存了物体的局部坐标轴在世界空间中的朝向以及缩放的倍率。这就是为什么你可以直接从一个矩阵里同时取出position和rotation。有一点需要注意引擎内部矩阵运算遵循固定的行向量或列向量约定。Unity的Matrix4x4在文档中强调“列向量优先”也就是说变换结果是矩阵左乘向量result M * vector4。这决定了后续所有矩阵乘法都严格区分顺序写反了得到的坐标空间完全不是你想的那个。2.2 从矩阵到Transform的“三角分解”拿到一个世界矩阵后真正做跟随还需要把它分解回position、rotation、scale三个部分因为Transform组件只认这三种字段。Unity没有内置一个万能的分解函数但提供了一个很接近的工具Matrix4x4.Decompose。它接收一个矩阵输出scale、rotation、position三要素。不过这个方法有坑——如果矩阵里含有非等比缩放x缩放2倍、y缩放1倍、z缩放3倍解出来的rotation依然可能是错的原因在于旋转矩阵和缩放矩阵混合在一起后单一四元数无法同时表达“旋转后的非等比缩放”。这是我在第5节问题排查中会展开的重点这里先埋个引子。日常如果不涉及非等比缩放直接用Decompose完全够用。如果涉及我更推荐直接绕开矩阵的旋转分解只对目标做四元数的常规运算避免掉进数学陷阱。3. 三种矩阵跟随的写法覆盖绝大多数需求3.1 方案一直接复制目标世界矩阵最简单的场景是物体需要和目标长得一模一样位置、朝向、缩放全部同步。比如一个“镜像物体”或者一个需要完全贴合布娃娃角色的碰撞体再或者一个由程序生成的、想要跟随主相机视角的辅助相机。using UnityEngine; public class SimpleMatrixFollower : MonoBehaviour { [SerializeField] private Transform target; private void LateUpdate() { if (target null) return; // target.localToWorldMatrix 是目标“本地空间 - 世界空间”的完整矩阵 Matrix4x4 worldMatrix target.localToWorldMatrix; transform.position worldMatrix.GetPosition(); // 注意这里直接取 rotation 在处理非等比缩放时有隐患稍后会讲 transform.rotation worldMatrix.rotation; transform.localScale worldMatrix.lossyScale; } }这段代码一看就懂但有两个细节需要说明。用LateUpdate而不是Update是为了尽量“延迟到渲染前再采样”避免目标在Update中移动后跟随物计算的位置先于目标被渲染造成一帧的视觉错位和抖动。这个属于跟随时非常典型的时序问题。GetPosition是Unity提供的便捷方法它从Matrix4x4里提取位置列比自己写m03、m13、m23更安全也更容易维护。3.2 方案二保持相对偏移的矩阵跟随这是矩阵方案最有价值的场景——物体跟随目标但要始终保持一个相对位置和相对旋转。最典型的就是载具上的座位挂点、角色背上的武器挂点、队伍里围绕队长呈圆形分布的队员。思路是先缓存在“跟随开始的那一帧”自身世界矩阵相对于目标世界矩阵的偏移之后每帧都用这个偏移乘上目标的当前世界矩阵得到自身的新世界矩阵。using UnityEngine; public class RelativeMatrixFollower : MonoBehaviour { [SerializeField] private Transform target; [SerializeField] private Vector3 positionOffset new Vector3(0f, 1f, -2f); [SerializeField] private Vector3 rotationOffset new Vector3(0f, 0f, 0f); private Matrix4x4 relativeMatrix; private void Start() { // 构造“初始相对矩阵”这个矩阵表示“自身在目标本地坐标系里的位置姿态” Matrix4x4 trs Matrix4x4.TRS(positionOffset, Quaternion.Euler(rotationOffset), Vector3.one); // 与目标世界矩阵相乘后得到的就是自身世界矩阵 relativeMatrix trs; } private void LateUpdate() { if (target null) return; // 自身世界矩阵 相对偏移矩阵 * 目标当前世界矩阵 // 注意乘法顺序不能反反了就变成“目标跟随自己” Matrix4x4 selfWorldMatrix relativeMatrix * target.localToWorldMatrix; // 分解矩阵写回 Transform Matrix4x4.Decompose(selfWorldMatrix, out Vector3 scale, out Quaternion rotation, out Vector3 position); transform.SetPositionAndRotation(position, rotation); transform.localScale scale; } }这里矩阵乘法顺序是本方案的核心。为什么是relativeMatrix * target.localToWorldMatrix简单理解先把目标世界矩阵作为基础空间再在这个空间上叠加一个“相对目标本地坐标系的偏移”。因为Unity是列向量优先乘法从右往左生效target.localToWorldMatrix先生效relativeMatrix后生效。你可以把relativeMatrix理解成“我相对于目标那个坐标系要偏移多少”。这个偏移是基于目标本地坐标系的——目标一转动你的位置也跟着转动就像挂在角色背上的武器一样。这一点是普通Transform赋值做不到的如果用position target.position的写法你只能在世界空间里算偏移目标转头时武器不会跟着转。如果需要动态修改相对偏移不用重新计算整个矩阵只需要用Matrix4x4.TRS根据新的PositionOffset和RotationOffset重新构建relativeMatrix就行。这也暴露了矩阵的一个好处——偏移逻辑被统一在了一个数据结构里不用同时维护好几个Vector3。3.3 方案三把矩阵分解写成通用工具实际项目里跟随逻辑经常会同时出现在好几个类里与其每个类都写一遍分解代码不如封装一个工具方法。using UnityEngine; public static class MatrixFollowUtility { /// summary /// 将 Matrix4x4 安全分解并写回 Transform /// 注意非等比缩放场景下 rotation 会不准确包含非等比缩放时请改用其他方案 /// /summary public static void ApplyMatrixToTransform(Transform targetTransform, Matrix4x4 worldMatrix) { Matrix4x4.Decompose(worldMatrix, out Vector3 scale, out Quaternion rotation, out Vector3 position); targetTransform.SetPositionAndRotation(position, rotation); targetTransform.localScale scale; } /// summary /// 计算一个物体相对于另一个物体的“世界相对矩阵” /// selfWorldMatrix 是自身的世界矩阵anchorWorldMatrix 是锚点/目标的世界矩阵 /// /summary public static Matrix4x4 CalculateRelativeMatrix(Matrix4x4 selfWorldMatrix, Matrix4x4 anchorWorldMatrix) { return selfWorldMatrix * anchorWorldMatrix.inverse; } }封装后的使用场景是这样的某型号的运载工具上要挂不同尺寸的箱子每个箱子都只需要在Start里调用一次CalculateRelativeMatrix拿到相对矩阵然后在LateUpdate里用方案二的乘法去做跟随。所有箱子共享同一套逻辑新加挂点零成本。有一个细节我特别想提醒锚点矩阵的inverse计算在Start阶段做一次就好不要每帧都在LateUpdate里做。虽说矩阵求逆的代价现在看起来不大但在有几百个跟随物时每帧一次逆运算会白白浪费大量CPU时间更稳妥的做法是缓存相对矩阵而不是每帧都重新求逆。4. 矩阵跟随必须注意的时序与空间问题4.1 LateUpdate与Update的选择直接决定画面是否抖动跟随逻辑放在Update还是LateUpdate在大多数项目里并不会立刻暴露问题。但当你把相机也放进场景并且相机自身逻辑也写在Update里时顺序就可能出错。我自己的做法是所有跟随物统一放在LateUpdate。因为Unity的执行顺序是先Update后LateUpdate目标在Update里的移动会在LateUpdate之前完成跟随物读取到的就是“目标这一帧最终的位置”而不是“上一帧尾巴上的位置”。渲染发生在所有Update和LateUpdate之后所以跟随物能拿到最新的目标状态画面节奏更顺。如果跟随的是相机目标尤其需要注意这一条。相机如果也需要跟随某个物体应当把相机的跟随逻辑放在所有普通跟随物之后执行或者在相机脚本的LateUpdate里做。总之一句话跟随逻辑永远不要在Update里和目标的移动逻辑抢时间。4.2 矩阵手性和坐标约定的常见失误Matrix4x4的乘法不是交换的A * B和B * A结果通常完全不同。放在跟随场景里如果乘法顺序反了常见结果就是物体会莫名其妙地“围着目标做反方向转动”或者位置一直对不上。排查时优先检查矩阵乘法顺序其次检查手性。Unity的坐标系是左手系旋转矩阵的构造和引擎内部的矩阵值都有特定约定。如果你在外部生成矩阵比如从某个数据结构里读到了欧拉角直接用Matrix4x4.TRS重新构造不要尝试手工拼矩阵手工拼出的矩阵在手性上翻车的概率极高。4.3 启用非等比缩放时矩阵旋转分解会失真这个坑值得单独写一节。如果目标物体在某个轴上有非等比缩放——比如一个门板x方向拉长了3倍y方向还是1——它的localToWorldMatrix里左上角3x3已经不是纯旋转矩阵而是旋转和缩放混合在一起的结果。这时候调用matrix.rotation或者Decompose提取出的四元数可能和你在Inspector里看到的rotation值不一致旋转表现会怪怪的。我的处理方案很简单跟随场景里如果目标有非等比缩放不要提取旋转而是直接只同步位置或者基于目标本身的rotation属性去做旋转运算绕开矩阵分解。位置提取一般不受非等比缩放影响因为位置就是最后一列的平移向量。4.4 矩阵方案的性能真相有人担心矩阵运算会比直接赋值慢很多。实测下来并不是这样。Matrix4x4的四则运算在Unity里经过高度优化对于一个跟随物来说每帧两三次矩阵乘法和一次Decompose的开销可以忽略不计。真正的性能压力来自滥用比如几百个物体同时在自己Update里对同一个目标求逆矩阵、反复调用Decompose去解只需要位置的矩阵。优化的两个原则一是能缓存的结果绝不在每帧重复计算比如相对矩阵只算一次二是能只取位置就直接GetPosition不要把整个矩阵拆成Transform再赋值拆的过程涉及更多数据搬运。遇到大量跟随物时还可以考虑用JobSystem批量处理矩阵数据但多数项目到这一步之前早就用别的方案优化了。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因排查方向跟随物飞到很远的位置或旋转乱转矩阵乘法顺序写反检查代码里relativeMatrix和target.localToWorldMatrix的先后顺序跟随物抖动、闪一下Update/LateUpdate执行顺序不匹配目标在Update中移动跟随逻辑移到LateUpdaterotation无法匹配目标角度目标有非等比缩放放弃矩阵旋转分解改用Transform的rotation属性做旋转同步目标动了跟随物纹丝不动target.localToWorldMatrix没有每帧刷新确认是否在Start里只缓存了一次矩阵需要改成每帧获取本身有父物体时同步后位置偏一层直接把世界矩阵赋值给localPosition把从矩阵提取的结果写进SetPositionAndRotation或者先转成相对父节点的本地矩阵5.2 我踩过的一个典型坑某次做队伍阵型Demo时我给每个队员都挂了一个矩阵跟随脚本目标都是队长。最开始一切正常但队员只要初始位置不在原点就会出现第一帧闪跳——队员从世界原点“弹”到目标旁边。排查了很久发现Start里计算相对矩阵时我下意识用了transform.localToWorldMatrix但此时物体的Transform数据还没初始化完毕matrix里读出来的位置是原点。这个问题的核心是在Start阶段引擎会先处理一部分初始化但如果你在Awake里修改了位置Start里读到的矩阵可能还是修改前的。稳妥做法是在Start里不要依赖自己当前的矩阵状态而是先手动写入一次想要的初始位置再计算相对矩阵或者推迟到第一次LateUpdate再初始化相对矩阵。5.3 不想处理这个坑的替代方案如果不是特别需要底层矩阵干预Unity官方也有更便捷的API可以简化跟随问题Transform.SetParent配合世界位置保持模式或者简单的DOTween、插值方案。但如果你做的是需要自定义偏移矩阵、动态切换跟随坐标系、或者批量处理多个跟随物的复杂系统矩阵方案依然是最稳、最可控的底层工具。我个人的习惯是初级跟随完全一致用方案一挂点跟随相对偏移用方案二启动阶段涉及动态空间换算时用方案三的封装。矩阵并不总是最快解法但它是“当麻烦出现时你能打开引擎看到底层逻辑”的那把钥匙。这里额外分享一个我自己很受用的小技巧跟随代码里所有涉及矩阵乘法的表达式写完后立刻在注释里标明坐标空间比如“相对矩阵 * 目标世界矩阵 自身世界矩阵”。项目过了一个月再回来看这一行注释能帮你省下大量回忆和调试时间。
阅读完成 · 觉得有帮助?