1. 为什么 Unity 开发者都绕不开 LeanTween
如果你做过 Unity 项目,尤其是 UI 动画、弹窗过渡、按钮反馈这类需求,大概率听过或者用过 LeanTween。它是一个轻量级的补间动画库,核心价值就一句话:用最少的代码,让物体动起来。相比 Unity 自带的 Animator 或者手写协程插值,LeanTween 的优势在于 API 极其简洁、运行时开销小、链式调用顺手,而且不需要额外的编辑器配置。
我第一次接触 LeanTween 是在一个卡牌游戏项目里,当时需要做大量卡牌的翻转、缩放、位移效果。用 Animator 做的话,每张卡牌都要配状态机,维护成本高得离谱;用协程写插值,代码又臭又长。后来换成 LeanTween,一行代码就能搞定一个动画,而且支持回调、延迟、循环、缓动曲线这些常用功能,开发效率直接翻倍。
这篇文章主要面向已经有一定 Unity 基础、准备在项目中引入或正在使用 LeanTween 的开发者。我会把 LeanTween 最常用的 API 按使用场景拆开讲,包括位移、旋转、缩放、透明度、颜色、数值插值、回调、缓动曲线、循环与暂停等,同时补充一些官方文档里不会写的实操细节和踩坑经验。读完你至少能做到:拿到一个动画需求,知道该用哪个 API,参数怎么配,以及为什么这么配。
2. LeanTween 的核心设计思路与选型考量
2.1 为什么是 LeanTween 而不是 DOTween 或 Animator
Unity 生态里做补间动画,主流选择有三个:Animator、DOTween、LeanTween。Animator 适合做复杂的角色动画和状态机驱动的动画,但用来做 UI 动效就太重了,而且改一个参数要开窗口调半天。DOTween 功能更全,社区也大,但免费版有一些限制,而且 API 风格偏链式,学习曲线略陡。LeanTween 的定位很明确:轻量、够用、快。
LeanTween 的核心实现是基于一个全局的更新循环,所有动画对象都放在一个列表里统一驱动,避免了每个动画都开协程带来的开销。它的 API 设计也很直接,大部分方法都是静态方法,调用方式类似LeanTween.moveX(gameObject, 5f, 1f),一看就懂。对于中小型项目或者 UI 动效为主的场景,LeanTween 的性价比非常高。
2.2 补间动画的本质:插值与缓动
补间动画说白了就是在一段时间内,把一个属性从 A 值平滑地变到 B 值。这个过程由两个核心部分组成:插值和时间曲线。插值决定了每一帧属性值怎么算,LeanTween 内部用的是线性插值,但通过缓动函数(easing)把线性时间映射成非线性时间,从而实现加速、减速、回弹等效果。
举个例子,LeanTween.moveX(gameObject, 5f, 1f)的意思是让物体在 1 秒内从当前 X 位置移动到 X=5。如果不加缓动,它就是匀速运动;如果加上setEase(LeanTweenType.easeOutQuad),它就会先快后慢,看起来更自然。理解这一点很重要,因为后面所有 API 的参数配置,本质上都是在控制“插值目标”和“时间曲线”。
2.3 LeanTween 的初始化与全局配置
LeanTween 不需要在场景里挂任何东西,但建议在项目启动时做一次初始化,尤其是需要控制全局时间缩放或者预分配内存的时候。常见的做法是在一个全局管理脚本的Awake里调用LeanTween.init(),参数是预估的最大同时动画数量。这个值如果设得太小,后面动画多了会动态扩容,带来一点性能抖动;设得太大又浪费内存。根据经验,UI 项目一般设 400 到 800 就够了,重度特效项目可以设到 1500。
void Awake() { LeanTween.init(600); }另外,LeanTween 默认使用 unscaledDeltaTime 还是 deltaTime,取决于你有没有调用LeanTween.setUseUnscaledTime。如果游戏有暂停功能,而你又希望暂停时 UI 动画继续播放,那就需要把相关动画设为不受时间缩放影响。
3. 位移、旋转、缩放三大基础 API 详解
3.1 位移 API:move、moveX、moveY、moveZ
位移是 LeanTween 用得最多的功能。最常用的是LeanTween.move,它接受一个 Vector3 作为目标位置,让物体在指定时间内移动到那里。如果你只想改一个轴,可以用moveX、moveY、moveZ,性能略好一点点,代码也更清晰。
// 移动到目标位置,耗时 0.5 秒 LeanTween.move(gameObject, new Vector3(100, 0, 0), 0.5f); // 只移动 X 轴到 200,耗时 0.3 秒 LeanTween.moveX(gameObject, 200f, 0.3f);这里有个细节:move的目标位置是世界坐标还是本地坐标?默认是世界坐标。如果你做 UI 动画,RectTransform 用的是 anchoredPosition,那就要用LeanTween.move的重载版本,传入RectTransform或者直接用moveLocal。我踩过的坑是:对 UI 元素用move结果位置偏了,后来才发现 UI 应该用LeanTween.move(rectTransform, anchoredPos, duration)这个重载。
注意:UI 元素的位移优先使用 RectTransform 重载,避免世界坐标和锚点坐标混用导致位置错乱。
3.2 旋转 API:rotate、rotateLocal、rotateAround
旋转 API 主要有三个:rotate按世界坐标旋转,rotateLocal按本地坐标旋转,rotateAround绕某个点旋转。做 UI 的话,rotateLocal更常用,因为 UI 的旋转通常是相对于自身的。
// 本地旋转到 180 度,耗时 0.4 秒 LeanTween.rotateLocal(gameObject, new Vector3(0, 0, 180), 0.4f); // 绕 Y 轴旋转到 90 度 LeanTween.rotateY(gameObject, 90f, 0.5f);旋转有一个容易忽略的点:LeanTween 默认使用四元数插值,所以不会出现万向锁问题,但如果你传入的是欧拉角,它会自动转换。另外,旋转的缓动曲线建议用easeInOutQuad或者easeOutBack,视觉效果更顺滑。
3.3 缩放 API:scale、scaleX、scaleY、scaleZ
缩放 API 和位移类似,scale接受 Vector3,scaleX等只改一个轴。做按钮点击反馈时,通常用scale从 1 缩到 0.9 再弹回 1,配合easeOutBack效果很好。
// 缩放到 0.9 倍,耗时 0.1 秒 LeanTween.scale(gameObject, Vector3.one * 0.9f, 0.1f) .setEase(LeanTweenType.easeOutBack) .setOnComplete(() => { LeanTween.scale(gameObject, Vector3.one, 0.1f); });这里用到了setOnComplete回调,后面会详细讲。缩放的一个常见问题是:如果物体的初始缩放不是 1,直接缩放到目标值可能会跳变。解决办法是先记录初始缩放,动画结束后再恢复。
4. 透明度、颜色与数值插值的高级用法
4.1 透明度 API:alpha 与 CanvasGroup
控制透明度有两个途径:一是直接改 UI 元素的 CanvasRenderer 的 alpha,二是改 CanvasGroup 的 alpha。LeanTween 提供了LeanTween.alpha和LeanTween.alphaCanvas两个方法。alpha用于普通 SpriteRenderer 或 Image,alphaCanvas用于 CanvasGroup。
// 将 Image 的透明度从当前值渐变到 0,耗时 0.3 秒 LeanTween.alpha(imageRectTransform, 0f, 0.3f); // 将 CanvasGroup 的透明度渐变到 1 LeanTween.alphaCanvas(canvasGroup, 1f, 0.3f);做弹窗淡入淡出时,推荐用 CanvasGroup,因为它可以一次性控制整个面板的所有子元素透明度,而且不会影响交互。如果只改单个 Image 的 alpha,子元素不会跟着变。
提示:CanvasGroup 的 alpha 为 0 时,默认仍然会接收射线,如果不想让隐藏的弹窗挡住点击,记得把
blocksRaycasts设为 false,或者用LeanTween.alphaCanvas的回调里手动处理。
4.2 颜色 API:color 与 color32
LeanTween.color可以让物体在指定时间内从当前颜色渐变到目标颜色,适用于 Image、Text、SpriteRenderer 等。它内部会修改材质的 color 属性,所以如果多个物体共用同一个材质,会出现颜色互相影响的问题。解决办法是给每个需要独立变色的物体使用材质实例。
// 将 Image 颜色渐变到红色,耗时 0.5 秒 LeanTween.color(imageRectTransform, Color.red, 0.5f);如果只是做 UI 颜色变化,更推荐用LeanTween.color的 Text 或 Image 重载,避免材质实例化带来的内存开销。另外,颜色渐变和透明度渐变可以同时进行,但要注意执行顺序,避免互相覆盖。
4.3 数值插值:value 与 delayedCall
LeanTween.value是一个被低估的 API,它可以在两个数值之间插值,并通过回调把当前值传出来。这个功能非常适合做进度条、倒计时、自定义属性动画。
// 从 0 到 100 插值,耗时 2 秒,每帧回调更新进度条 LeanTween.value(gameObject, 0f, 100f, 2f) .setOnUpdate((float val) => { progressBar.fillAmount = val / 100f; });delayedCall则是延迟执行一个回调,相当于Invoke的 LeanTween 版本,但更灵活,因为它可以取消、可以链式调用。
// 延迟 1 秒后执行 LeanTween.delayedCall(1f, () => { Debug.Log("延迟执行"); });这两个 API 组合起来,可以实现很多复杂的动画序列,比如先延迟、再插值、最后回调。
5. 回调、缓动曲线与循环控制的实战配置
5.1 回调体系:onComplete、onStart、onUpdate
LeanTween 的回调体系很完整,常用的有setOnStart、setOnUpdate、setOnComplete。setOnStart在动画开始时触发一次,setOnUpdate每帧触发,setOnComplete在动画结束时触发一次。
LeanTween.moveX(gameObject, 5f, 1f) .setOnStart(() => Debug.Log("开始移动")) .setOnUpdate((float val) => Debug.Log("当前位置:" + val)) .setOnComplete(() => Debug.Log("移动结束"));这里有个坑:setOnUpdate的参数是当前插值,不是物体的实际位置。如果你想知道物体实际位置,得在回调里自己取transform.position。另外,setOnComplete在动画被取消时不会触发,如果需要处理取消逻辑,要用setOnCompleteOnRepeat或者手动管理。
5.2 缓动曲线:LeanTweenType 全解析
LeanTween 内置了三十多种缓动曲线,常用的有:
| 缓动类型 | 效果 | 适用场景 |
|---|---|---|
| easeInQuad | 先慢后快 | 入场动画 |
| easeOutQuad | 先快后慢 | 出场动画 |
| easeInOutQuad | 两头慢中间快 | 循环动画 |
| easeOutBack | 超出再回弹 | 按钮点击 |
| easeOutBounce | 弹跳效果 | 掉落动画 |
| easeInOutBack | 两头回弹 | 弹窗弹出 |
选择缓动曲线的原则是:入场用 easeOut,出场用 easeIn,循环用 easeInOut,需要强调用 back 或 bounce。不要所有动画都用 linear,那样看起来很机械。
5.3 循环与暂停:setLoop、setRepeat、pause、resume
setLoop可以让动画循环播放,参数是循环次数,-1 表示无限循环。setRepeat类似,但可以设置每次循环之间的间隔。
// 无限循环缩放 LeanTween.scale(gameObject, Vector3.one * 1.2f, 0.5f) .setLoopPingPong(-1);setLoopPingPong是来回循环,适合做呼吸灯效果。暂停和恢复用LeanTween.pause和LeanTween.resume,可以传入 gameObject 或动画 ID。
// 暂停某个物体的所有动画 LeanTween.pause(gameObject); // 恢复 LeanTween.resume(gameObject);这里要注意:pause和resume是基于动画 ID 的,如果你用LeanTween.move返回的LTDescr对象来管理,会更精确。
6. 常见问题与排查技巧实录
6.1 动画不执行或执行一次就失效
这是最常见的问题,通常有三个原因:一是动画目标物体被销毁了,LeanTween 内部还持有引用,导致报错;二是动画被其他动画覆盖了,比如同一个属性同时有两个动画在跑;三是LeanTween.init没有调用,或者最大动画数量设得太小。
排查方法:先看 Console 有没有报错,然后检查物体是否 active,最后确认动画 ID 是否被重复使用。如果同一个物体同一个属性需要连续动画,建议用setOnComplete链式调用,而不是同时启动两个动画。
6.2 UI 动画位置偏移或缩放异常
UI 动画最常见的问题是坐标系混用。RectTransform 的 anchoredPosition 和 transform.position 是两套坐标系,如果用LeanTween.move传世界坐标,UI 就会偏。解决办法是统一用 RectTransform 重载,或者用moveLocal。
另一个问题是缩放基准。如果 UI 元素的初始缩放不是 1,直接LeanTween.scale到目标值会跳变。建议先记录初始缩放,动画结束后恢复。
6.3 动画卡顿或性能下降
LeanTween 本身性能很好,但如果同时运行的动画太多,或者每个动画都开了回调,会有一定开销。优化方法:合并同类动画,减少setOnUpdate的使用,尽量用setOnComplete代替每帧回调。另外,如果动画不需要在暂停时继续,记得用LeanTween.setUseUnscaledTime控制时间源。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 动画不执行 | 物体被销毁或未初始化 | 检查物体状态,调用 LeanTween.init |
| 位置偏移 | 坐标系混用 | UI 用 RectTransform 重载 |
| 缩放跳变 | 初始缩放不是 1 | 记录初始值,动画后恢复 |
| 卡顿 | 动画数量过多 | 合并动画,减少回调 |
| 回调不触发 | 动画被取消 | 用 setOnCompleteOnRepeat 或手动管理 |
6.4 动画取消与内存泄漏
LeanTween 的动画如果没跑完就切换场景,可能会残留引用。建议在场景切换前调用LeanTween.cancelAll或者LeanTween.cancel(gameObject)。另外,如果动画持有闭包回调,而回调里引用了外部对象,要注意及时释放,避免内存泄漏。
void OnDestroy() { LeanTween.cancel(gameObject); }这个习惯能避免很多莫名其妙的报错。
7. 我在实际项目中的一些使用体会
LeanTween 用久了会发现,它的价值不在于功能多强大,而在于“刚刚好”。大部分 UI 动效需求,它都能用一两行代码解决,而且性能可控。我个人的习惯是:简单的位移、缩放、透明度变化直接用 LeanTween;复杂的序列动画用LeanTween.sequence或者链式回调;需要精确控制的用LTDescr对象管理。
另外一个小技巧:如果你需要做一串动画,比如先移动、再旋转、最后缩放,可以用LeanTween.sequence把它们串起来,代码会比嵌套回调清晰很多。
LeanTween.sequence() .append(LeanTween.moveX(gameObject, 5f, 0.5f)) .append(LeanTween.rotateZ(gameObject, 90f, 0.3f)) .append(LeanTween.scale(gameObject, Vector3.one * 1.5f, 0.2f));最后再分享一个排查技巧:如果动画表现和预期不符,先检查缓动曲线,再检查时间源,最后检查坐标系。这三个地方对了,基本就不会出大问题。