
1. 项目概述为什么需要“平滑移动”在Unity开发中尤其是涉及角色控制、相机跟随、UI动画或者任何需要物体从A点移动到B点的场景里一个最常见也最容易被忽视的细节就是移动的“手感”。新手开发者最容易犯的错误就是直接使用transform.position targetPosition或者transform.Translate配合固定速度结果就是物体像瞬间传送或者机器人一样僵硬地移动毫无美感与沉浸感。这就是Vector3.SmoothDamp要解决的核心问题实现带有平滑过渡、速度连续变化、最终能精确停在目标点的移动效果。想象一下相机跟随玩家如果相机是“瞬移”到玩家身后的玩家会感到晕眩如果是一个宝箱缓缓打开盖子盖子应该是先加速再减速而不是“啪”一下弹开。这些场景都需要平滑插值。SmoothDamp直译过来就是“平滑阻尼”它本质上是一个基于物理阻尼模型的插值函数。与线性插值Lerp不同SmoothDamp会模拟一个逐渐衰减的速度过程让物体的移动拥有“惯性”和“缓冲”运动曲线是平滑的S形缓入缓出视觉效果非常自然。它特别适合用于每帧更新的Update函数中处理跟随、追踪等动态目标。对于谁需要看这篇内容如果你是Unity初学者正在为角色移动生硬而烦恼或者你是有经验的开发者想深入理解SmoothDamp与Lerp、MoveTowards的区别及内部原理并掌握其所有参数细节和高级应用技巧那么这篇从一线实战中总结的深度解析就是为你准备的。2. 核心原理与函数参数深度拆解理解Vector3.SmoothDamp不能停留在“会用”的层面必须吃透它的每一个参数才能在各种复杂场景下游刃有余。2.1 函数签名与参数精讲Vector3.SmoothDamp的标准签名如下public static Vector3 SmoothDamp(Vector3 current, Vector3 target, ref Vector3 currentVelocity, float smoothTime, float maxSpeed Mathf.Infinity, float deltaTime Time.deltaTime);这六个参数每一个都至关重要current (Vector3)物体的当前位置。通常传入transform.position。target (Vector3)希望物体移动到的目标位置。这个位置可以是静态的也可以是动态变化的如另一个物体的位置。currentVelocity (ref Vector3)这是理解SmoothDamp的关键这是一个引用参数函数会在内部修改它。它代表物体当前的运动速度向量。你需要在函数外部声明一个Vector3变量例如velocity并传入函数每帧会根据平滑计算更新这个速度值。你必须将其定义在Update函数之外作为类的成员变量否则每帧速度都会被重置平滑效果将失效。smoothTime (float)近似达到目标所需的时间以秒为单位。这是最容易被误解的参数。它不是一个精确的定时器而是一个控制平滑“缓急”程度的参数。较小的值如0.1f会导致更快速的移动但仍平滑较大的值如1.0f会导致非常缓慢、悠长的移动。它本质上影响了速度衰减的快慢。maxSpeed (float)可选参数速度的绝对值上限。默认是无穷大。这是一个安全阀当目标位置突然发生极远距离跳变时可以防止计算出的理论速度值过大导致物体“飞”得过快甚至出现数值问题。在大多数跟随场景中合理设置此值如10或20是个好习惯。deltaTime (float)可选参数时间增量。默认使用Time.deltaTime这意味着移动速度与帧率无关。除非你有特殊需求如使用固定时间步长进行物理相关计算否则不要改动它。2.2 与 Lerp、MoveTowards 的核心区别很多新手会混淆这三个函数选择错误会导致效果南辕北辙。函数核心行为运动曲线是否依赖速度状态典型应用场景Vector3.Lerp线性插值。根据第三个参数t(0到1) 在两点间取一个线性中间点。t是比例不是速度。直接Lerp(current, target, 0.1f)在Update中会导致移动越来越慢永远到不了终点除非t1。线性如果t恒定否UI颜色渐变、静态对象之间的过渡。不适用于每帧跟踪动态目标的平滑移动。Vector3.MoveTowards匀速移动。以指定的最大步长向目标移动。移动速度是恒定的到达终点时会立刻停止没有缓冲。折线/匀速否需要精确控制每帧最大移动距离的场景如网格化移动、防止穿透。移动“手感”较硬。Vector3.SmoothDamp平滑阻尼移动。基于当前速度模拟物理阻尼速度会从初始值平滑衰减到0。运动是连续的有加速度和减速度过程。平滑的S型曲线缓入缓出是依赖currentVelocity状态相机跟随、角色平滑转向、物体缓动到目标点、任何需要自然平滑移动的地方。一个关键心法Lerp插值的是位置基于比例而SmoothDamp插值的是速度基于时间。SmoothDamp内部在解一个微分方程这才是它产生自然运动曲线的根本原因。2.3 底层数学模型浅析如果你对为什么SmoothDamp能产生平滑曲线感兴趣这里有一个简化的概念模型。它模拟的是一个“临界阻尼弹簧”系统。想象物体通过一个弹簧连接到目标点当前速度 (currentVelocity)物体当前的惯性。平滑时间 (smoothTime)与弹簧的“刚度”和“阻尼系数”相关共同决定了系统恢复到平衡状态到达目标的快慢和振荡情况。函数计算每一帧它根据当前位置与目标的距离、当前速度以及平滑时间计算出一个新的、更接近目标的速度同时满足阻尼条件避免过度振荡然后用这个新速度来更新位置。Unity的源码实现虽然我们看不到官方源码但标准算法类似会确保系统处于临界阻尼或过阻尼状态这意味着物体会以最有效率的方式平滑接近目标而不会来回摆动欠阻尼。这也是为什么它比单纯用Lerp自己模拟缓动函数更可靠、效果更好的原因。3. 基础到高级应用场景全实操理解了原理我们来实战。我会从最简单的场景开始逐步增加复杂度并分享每个步骤中的注意事项和调试技巧。3.1 基础应用让一个物体平滑移动到固定点这是最直接的用法常用于触发机关、UI弹窗入场等。using UnityEngine; public class SmoothMoveToPoint : MonoBehaviour { public Vector3 targetPosition; // 在Inspector中设置目标点 private Vector3 velocity Vector3.zero; // 关键声明速度变量 public float smoothTime 0.3f; // 平滑时间可调 void Update() { // 核心一行代码 transform.position Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); } }操作与调试将脚本挂载到场景中的一个物体比如一个Cube上。在Inspector中设置targetPosition为一个不同于物体初始位置的值如 (5, 0, 0)。运行游戏观察Cube的移动。你会看到它启动时慢慢加速接近终点时慢慢减速最后稳稳停住。调整smoothTime将其改为0.1f移动变快改为1.0f移动变慢。感受这个参数对“节奏”的控制。可视化调试在Update中添加Debug.DrawLine(transform.position, targetPosition, Color.green);可以实时看到物体与目标的连线帮助理解。3.2 动态目标跟随相机平滑跟随玩家这是SmoothDamp的招牌应用。相机的目标位置是玩家位置加上一个偏移量并且这个目标每帧都在变化。using UnityEngine; public class SmoothCameraFollow : MonoBehaviour { public Transform target; // 玩家的Transform public Vector3 offset new Vector3(0, 2, -5); // 相机相对于玩家的偏移 private Vector3 velocity Vector3.zero; public float smoothTime 0.2f; // 相机跟随通常需要更快的响应 void LateUpdate() // 使用LateUpdate确保在玩家移动之后更新相机 { if (target null) return; // 计算本帧相机希望到达的目标位置 Vector3 targetPosition target.position offset; // 应用平滑阻尼移动 transform.position Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); // 让相机始终看着玩家可选 transform.LookAt(target); } }实操心得为什么用LateUpdate确保玩家或其他跟随目标在本帧的所有位置更新都完成后相机再计算跟随避免出现相机抖动或滞后一帧的视觉问题。smoothTime的选择对于相机跟随0.1f到0.3f是比较常用的范围。值太小如0.05f相机会显得“紧张”有轻微抖动值太大如0.5f相机会有严重的“拖尾”感在玩家急停急转时尤其明显。需要根据游戏类型快节奏FPS还是慢节奏RPG调整。分离轴向控制有时你希望相机在水平方向XZ和垂直方向Y上有不同的跟随灵敏度。你可以对每个轴单独使用Mathf.SmoothDampVector3.SmoothDamp的标量版本。float velocityX 0, velocityY 0, velocityZ 0; float posX Mathf.SmoothDamp(transform.position.x, targetPos.x, ref velocityX, smoothTimeX); float posY Mathf.SmoothDamp(transform.position.y, targetPos.y, ref velocityY, smoothTimeY); float posZ Mathf.SmoothDamp(transform.position.z, targetPos.z, ref velocityZ, smoothTimeZ); transform.position new Vector3(posX, posY, posZ);3.3 高级应用实现带延迟的“弹性”跟随有时我们不想让跟随者死死咬住目标而是希望它有一点延迟和弹性比如一些RTS游戏的镜头或者一个跟在主角身后的宠物。这需要对SmoothDamp计算出的位置进行二次处理。思路我们不是直接跟随目标当前位置而是跟随一个“理想位置”。这个“理想位置”本身也用一个SmoothDamp向真实目标位置移动但速度更慢。using UnityEngine; public class ElasticFollower : MonoBehaviour { public Transform target; public float followSharpness 0.1f; // 跟随者自身的平滑度快 public float targetOffsetSharpness 0.05f; // 理想位置追赶目标的平滑度慢 private Vector3 currentVelocity Vector3.zero; private Vector3 targetOffsetVelocity Vector3.zero; private Vector3 idealFollowPosition; // 跟随者真正要去的位置 void Start() { if (target ! null) idealFollowPosition target.position; } void LateUpdate() { if (target null) return; // 第一步理想位置缓慢地追赶真实目标位置 idealFollowPosition Vector3.SmoothDamp(idealFollowPosition, target.position, ref targetOffsetVelocity, targetOffsetSharpness); // 第二步跟随者快速地移动到理想位置 transform.position Vector3.SmoothDamp(transform.position, idealFollowPosition, ref currentVelocity, followSharpness); } }这种双重阻尼的效果就是当目标突然移动时跟随者会先“愣一下”然后开始加速追赶并且在目标停下后它还会因为惯性稍微“冲过头”一点再摆回来形成非常生动的弹性效果。通过调整两个sharpness这里用了平滑时间的倒数概念值越小越平滑参数你可以精确控制延迟量和弹性大小。4. 参数调优与性能陷阱规避SmoothDamp用起来简单但想调出最佳手感并避免性能问题需要一些经验。4.1 SmoothTime 的“艺术”smoothTime不是精确的到达时间。一个经验公式是实际到达95%距离所需时间大约为smoothTime * 3。如果你需要更精确的时间控制SmoothDamp可能不是最佳选择可以考虑使用AnimationCurve配合Lerp或者DOTween等插件。调试技巧在场景中放置两个物体一个作为起点一个作为终点。运行脚本用Time.time记录开始移动的时间并在Update中计算当前距离与总距离的比例当比例大于0.95时输出耗时。多次测试不同smoothTime值你就能找到手感与时间的对应关系。4.2 MaxSpeed不可或缺的安全阀永远不要忽视maxSpeed。考虑这个场景你的相机跟随玩家玩家突然死亡重生在地图另一端。此时current和target的距离极大计算出的瞬时速度可能是一个天文数字导致相机在下一帧“瞬移”到异常远的地方甚至可能因为浮点数问题导致崩溃。// 良好的实践总是设置一个合理的最大速度 float maxSpeed 50f; // 根据你的游戏世界尺度设定 transform.position Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime, maxSpeed);我的经验是对于角色或相机移动maxSpeed可以设为正常移动速度的5-10倍。这既允许了快速的响应又杜绝了极端情况下的bug。4.3 性能考量与 Ref 参数的本质ref Vector3 currentVelocity这个参数设计得非常巧妙。它通过引用传递使得函数能够修改调用者的变量从而在帧与帧之间维持了“速度状态”。这个状态是实现平滑的核心。从性能角度看SmoothDamp内部涉及一些数学计算平方根、指数衰减计算等但单次调用开销很小。真正的性能陷阱在于调用频率和对象数量。不要在成千上万个物体的Update中同时调用。如果需要大量物体的平滑移动比如一群鱼考虑使用 Job System 或 ECS 进行批处理计算或者降低更新频率如每2帧更新一次。对于不活跃的对象及时禁用脚本。如果一个物体已经到达目标附近速度接近于零并且目标不再移动可以考虑停止调用SmoothDamp以节省CPU周期。可以通过判断velocity.sqrMagnitude是否小于一个极小阈值如0.001f来实现。5. 常见问题、坑点排查与替代方案即使理解了原理实战中还是会踩坑。下面是我总结的“血泪”清单。5.1 问题排查速查表现象可能原因解决方案物体根本不动1.currentVelocity变量被定义在Update方法内部每帧重置。2.smoothTime值过大比如100f。3.target位置就是当前位置。1. 将velocity声明为类成员变量。2. 减小smoothTime到合理范围0.01f~5f。3. 检查目标位置赋值逻辑。移动卡顿、抖动1. 在Update中操作Transform但帧率不稳定。2. 目标位置 (target) 本身也在剧烈抖动。3. 与物理引擎 (Rigidbody) 冲突。1. 确保使用Time.deltaTime默认。对于相机使用LateUpdate。2. 平滑目标位置本身或检查目标对象的移动逻辑。3. 如果物体有Rigidbody需要修改其velocity而非直接改position或使用Rigidbody.MovePosition。移动过快像瞬移1. 没有设置maxSpeed且目标位置突变距离极大。2.smoothTime值过小如0.001f。1. 始终设置一个合理的maxSpeed。2. 增大smoothTime。移动过慢永远到不了1.smoothTime值过大。2.maxSpeed设置得过小。1. 减小smoothTime。2. 增大maxSpeed或检查其是否被意外设置。到达终点后轻微来回晃动这是欠阻尼的轻微表现因为计算精度和浮点数误差导致。当物体非常接近目标时手动将其位置设置为目标位置并重置速度为零。只在某个轴上移动不正常目标位置在该轴上的值异常如未初始化或者对该轴做了特殊限制如2D游戏忽略了Z轴。检查目标位置向量的每个分量。在2D游戏中确保Z轴被锁定或正确处理。5.2 浮点数精度与停止判断由于浮点数计算存在精度限制物体可能永远无法“精确”到达目标。通常我们需要一个停止条件。void Update() { Vector3 newPos Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); // 判断是否已经非常接近目标 if ((newPos - targetPosition).sqrMagnitude 0.0001f velocity.sqrMagnitude 0.0001f) { // 直接锁定到目标并重置速度避免不必要的后续计算 transform.position targetPosition; velocity Vector3.zero; // 可以在这里触发一个“到达目的地”的事件 // OnReachedDestination?.Invoke(); } else { transform.position newPos; } }使用sqrMagnitude平方长度进行比较比magnitude开方长度性能更好。5.3 何时不该用 SmoothDampSmoothDamp虽好但并非万能。需要严格时间控制的动画比如一个精确持续2秒的过场动画。应使用Animation系统、Animator、或DOTween/LeanTween等插件它们提供基于时间的精确插值。物理模拟下的运动如果物体受物理引擎驱动有Rigidbody直接修改transform.position会破坏物理模拟。应使用Rigidbody.velocity或Rigidbody.AddForce并配合SmoothDamp来计算每帧期望的速度值再施加给刚体。极其复杂的运动路径需要沿贝塞尔曲线、样条曲线移动。SmoothDamp只能处理点到点的直线运动复杂路径需要专门的路径跟随算法。5.4 进阶替代与扩展当你需要更多控制时可以探索Mathf.SmoothDampAngle用于平滑角度如旋转它能正确处理360度环绕例如从350度平滑转到10度。自己实现阻尼函数如果你需要不同的缓动曲线如BounceOut, ElasticOut可以查阅罗伯特·彭纳的缓动函数自己实现一个类似SmoothDamp的封装。这能给你最大的灵活性。使用DOTween插件对于复杂的序列动画、并行动画、回调控制DOTween的DOMove方法提供了极其强大和易用的API其背后也包含了丰富的缓动函数是生产环境的高效选择。