ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity动画开发实战:DOTween核心原理、性能优化与Pro版高级应用

Unity动画开发实战:DOTween核心原理、性能优化与Pro版高级应用 1. 项目概述为什么Unity开发者绕不开DOTween如果你在Unity里做过动画肯定经历过用Mathf.Lerp手搓缓动曲线或者被iTween那略显古老的API折磨的时刻。几年前当我接手一个需要大量UI动画和对象移动的项目时面对动辄几十个需要平滑过渡的元素传统的协程配合线性插值不仅代码臃肿性能开销也让人头疼。直到我遇到了DOTween它几乎重塑了我对Unity中实现动画的认知。这不是一个简单的“工具”而是一套完整的、声明式的动画编程范式。简单来说DOTween是一个专注于“补间动画”Tweening的高性能插件。它的核心价值在于用极其简洁的链式API将你从繁琐的动画状态管理和每帧计算中解放出来。你不再需要写Update函数去每帧修改一个物体的位置或颜色只需要一行代码告诉DOTween“把那个物体用2秒钟以‘弹性出入’的缓动方式移动到(10, 5, 0)的位置。” 剩下的所有事情——包括帧率平滑、时间缩放、循环控制、回调触发——它全包了。而DOTween Pro可以理解为DOTween的“专业版”或“完整版”。免费开源的DOTween核心库功能已经非常强大足以应对90%的日常需求。但DOTween Pro在此基础上额外提供了一套强大的可视化编辑工具DOTween Visual Editor、一个用于路径动画的专用模块DOTweenPath以及一些高级特性如材质属性动画、文本动画等。对于需要快速原型验证、或者团队中策划、美术也想参与动画设计的项目来说Pro版的视觉化能力是巨大的生产力提升。这个实战指南就是基于我多年在移动端、PC和WebGL项目中使用DOTween的经验从核心概念、基础API、性能优化到Pro版工具的高级玩法进行一次系统性的梳理。无论你是刚接触Unity动画的新手还是想寻找更优雅解决方案的老手都能在这里找到可以直接“抄作业”的代码和避坑指南。2. 核心概念与设计哲学拆解2.1 什么是“补间动画”与“插值”在深入DOTween之前必须理解两个基石概念。补间动画这个词源于传统动画制作原指由主画师绘制关键帧Keyframe再由助手绘制中间帧In-between frame的过程。在程序世界里我们定义动画的起始状态A点和结束状态B点然后由计算机自动计算出中间所有过渡状态的过程就是补间。插值则是实现补间的数学方法。它根据一个从0到1的标准化时间通常我们叫它t或progress计算出介于A和B之间的一个值。最简单的就是线性插值Lerp结果 A (B - A) * t。当t0结果是At1结果是Bt0.5结果就是A和B的正中间。DOTween的强大在于它将几十种复杂的缓动函数Easing Function封装了起来。线性插值虽然简单但运动起来僵硬、不自然。缓动函数通过修改t的变化曲线模拟出物理世界中的加速、减速、弹性、反弹等效果。例如Ease.OutBack会让物体在到达终点前稍微“冲过头”一点再弹回来模拟一种果冻般的弹性感。DOTween内置了诸如InSine、OutExpo、InOutElastic等丰富的缓动类型这是手写代码难以比拟的便利。2.2 DOTween的链式API与流畅语法DOTween的API设计深受jQuery和LINQ的影响核心思想是链式调用。一个基本的动画序列看起来是这样的transform.DOMove(new Vector3(10, 0, 0), 1f) // 第一步移动到(10,0,0)用时1秒 .SetEase(Ease.OutBounce) // 第二步设置缓动类型为“反弹” .OnComplete(() Debug.Log(移动完成)) // 第三步设置完成回调 .SetLoops(2, LoopType.Yoyo); // 第四步设置循环2次并以“悠悠球”方式往返这行代码完成了一个包含移动、特效、回调、循环的复杂动画。链式调用的好处是可读性极强一眼就能看出动画的执行流程。同时它也是高度可组合的你可以轻松地将多个动画串联Sequence或并联同时启动多个Tween。2.3 DOTween Pro的价值主张可视化与生产力免费版DOTween已经解决了“怎么写”的问题而DOTween Pro要解决的是“怎么更快、更直观地做”的问题。它的核心价值体现在两个地方DOTween Visual Editor这是一个集成在Unity Inspector面板中的编辑器。你可以在不写一行代码的情况下为GameObject添加并配置动画。比如选中一个UI图片在Inspector中点击“Add Tween”选择动画属性如Scale设置起始值、结束值、持续时间、缓动类型甚至预览动画。这对于策划调整数值、美术微调效果来说是革命性的。动画配置可以作为组件保存实现数据与逻辑的分离。DOTweenPath制作物体沿复杂贝塞尔曲线路径运动的利器。你只需要在场景中拖动路径点物体就会平滑地沿着这条路径移动。在制作镜头运动、飞行轨迹、UI元素入场曲线时它比用代码计算位置要高效十倍不止。Pro版不是代码能力的升级而是工作流和协作效率的升级。在强调快速迭代的现代游戏开发中能让非程序员参与动画制作其价值不可估量。3. 从零到一DOTween基础实战全解析3.1 环境准备与初始化首先你需要获取DOTween。最推荐的方式是通过Unity的Package Manager从GitHub URL添加https://github.com/Demigiant/dotween.git。这种方式便于版本管理和更新。当然你也可以从Asset Store下载免费版本。安装后在任何脚本使用DOTween API前必须进行初始化。最佳实践是在游戏启动时如Awake方法中调用using DG.Tweening; // 引入命名空间 void Awake() { // 初始化DOTween引擎。建议设置一些全局默认值提升开发体验。 DOTween.Init(recycleAllByDefault: true, useSafeMode: true, logBehaviour: LogBehaviour.ErrorsOnly); // 设置DOTween的全局时间缩放因子这在实现游戏暂停时的动画暂停时非常有用。 // DOTween.timeScale 1f; // 默认就是1 }这里有两个关键参数recycleAllByDefault: true 开启全局Tween回收。DOTween内部有一个Tween对象池。开启后当一个Tween完成时它不会被销毁而是被回收到池子里等待下次被相同类型的动画复用。这能显著减少GC垃圾回收压力是移动端性能优化的关键一步。useSafeMode: true 安全模式。当Tween的目标比如你要移动的那个transform在动画播放期间被销毁了安全模式能防止抛出空引用异常而是优雅地终止该Tween。在项目稳定性要求高的场景下务必开启。实操心得很多新手会忽略初始化或者把初始化代码放在一个可能不会被最早执行的地方。我习惯在一个名为GameManager或Bootstrapper的启动脚本中做这件事确保万无一失。曾经在WebGL项目里因为初始化顺序问题导致动画不执行排查了半天。3.2 核心APITransform、Material与UI动画DOTween为几乎所有常见的可动画属性提供了扩展方法。让我们看几个最常用的例子。1. Transform动画// 移动 (DOMove) transform.DOMove(new Vector3(5, 2, 0), 1.5f); // 世界坐标移动 transform.DOLocalMove(Vector3.one * 2, 1f); // 本地坐标相对于父物体移动 // 旋转 (DORotate) transform.DORotate(new Vector3(0, 360, 0), 2f, RotateMode.FastBeyond360); // 绕Y轴旋转360度 // RotateMode.FastBeyond360 允许旋转超过360度而不是绕回0度动画更平滑。 // 缩放 (DOScale) transform.DOScale(Vector3.one * 1.5f, 0.5f); // 放大到1.5倍 transform.DOPunchScale(Vector3.one * 0.5f, 0.3f); // 脉冲缩放效果常用于按钮点击反馈2. Material动画颜色与浮点数Material mat GetComponentRenderer().material; // 改变颜色 (DOColor) mat.DOColor(Color.red, _Color, 1f); // 改变主颜色 mat.DOColor(Color.blue, _EmissionColor, 1f); // 改变自发光颜色需要材质支持 // 改变浮点属性 (DOFloat) mat.DOFloat(0.5f, _Metallic, 1f); // 改变金属度 mat.DOFloat(1f, _FresnelPower, 2f); // 改变菲涅尔强度用于制作边缘发光动画3. UI动画 (需要引入DG.Tweening的UI扩展模块using DG.Tweening.Core; using DG.Tweening.Plugins.Core.PathCore;通常安装后自动包含):using UnityEngine.UI; Image img GetComponentImage(); Text txt GetComponentText(); RectTransform rect GetComponentRectTransform(); // UI颜色与透明度 img.DOColor(new Color(1,1,1,0.5f), 0.5f); // 颜色渐变 img.DOFade(0, 0.5f); // 仅透明度渐变 // 文本动画 txt.DOText(这是一段逐字显示的文字, 2f); // 打字机效果 txt.DOBlendableColor(Color.green, 1f); // 可混合的颜色变化 // RectTransform专属动画 rect.DOAnchorPos(new Vector2(100, -100), 1f); // 锚点位置移动UI系统 rect.DOSizeDelta(new Vector2(200, 100), 0.7f); // 改变RectTransform的大小3.3 动画序列Sequence与并行控制单个动画很简单但复杂的动画流程需要组合。Sequence序列是DOTween中用于编排动画顺序的核心工具。你可以把它想象成一个动画列表里面的动画可以按顺序执行也可以插入间隔。Sequence mySequence DOTween.Sequence(); // 1. 首先将物体放大 mySequence.Append(transform.DOScale(1.5f, 0.5f)); // 2. 然后同时执行移动和旋转在同一个Append里Join mySequence.Append(transform.DOMoveX(5, 1f)); mySequence.Join(transform.DORotate(new Vector3(0, 180, 0), 1f)); // Join会让这个动画与上一个Append的动画同时开始 // 3. 插入一个0.5秒的空白间隔 mySequence.AppendInterval(0.5f); // 4. 最后缩小并褪色假设有Renderer mySequence.Append(transform.DOScale(1f, 0.3f)); mySequence.Join(GetComponentRenderer().material.DOFade(0, 0.3f)); // 设置整个序列循环 mySequence.SetLoops(-1, LoopType.Yoyo); // -1表示无限循环Yoyo表示像悠悠球一样来回播放 // 播放序列 mySequence.Play();并行控制除了用Sequence.Join更简单的方法是直接创建多个独立的Tween它们会自动同时运行。DOTween的底层调度器会高效地管理它们。3.4 回调函数与动画生命周期动画不是孤立的它需要与游戏逻辑联动。DOTween提供了丰富的回调函数。Tween myTween transform.DOMoveX(10, 2f) .OnStart(() { Debug.Log(动画开始); // 可以在这里播放音效或激活某个状态 }) .OnUpdate(() { // 每一帧都会调用可以在这里根据动画进度做实时计算 // 注意此回调频率很高避免在此处做耗时操作 float progress myTween.ElapsedPercentage(); // 获取已过去时间的百分比 }) .OnPlay(() Debug.Log(动画播放/继续)) // 每次调用Play()时触发 .OnPause(() Debug.Log(动画暂停)) .OnStepComplete(() Debug.Log(一个循环周期完成)) // 在每次循环结束时触发 .OnComplete(() { Debug.Log(动画全部完成); // 动画彻底结束后执行后续逻辑如跳转场景、销毁物体等 }) .OnKill(() Debug.Log(动画被强制终止)); // 调用Kill()时触发生命周期管理至关重要。一个常见的错误是在物体如一个敌人被销毁时没有终止其身上正在运行的Tween这会导致DOTween尝试访问一个已销毁的对象从而引发错误除非开启了安全模式。正确的做法是void OnDestroy() { // 终止此物体上所有由DOTween创建的动画 transform.DOKill(); // 或者如果你持有Tween的引用 // myTween?.Kill(); }4. 高级技巧与性能优化实战4.1 对象池与内存管理正如初始化部分提到的DOTween内置了对象池。但为了最大化性能尤其是对于高频创建和销毁的动画如子弹轨迹、UI点击特效我们需要更主动地管理。1. 复用Tween不要每次需要动画时都new一个。对于重复使用的动画模式可以预先创建并存储起来。Tween cachedScaleTween; void Start() { // 预创建一个缩放动画但不立即播放 cachedScaleTween transform.DOScale(1.2f, 0.2f) .SetAutoKill(false) // 关键防止动画完成后自动被回收销毁 .Pause(); // 创建后暂停 } void OnButtonClicked() { // 每次需要时重启这个缓存的动画 cachedScaleTween.Restart(); }2. 使用SetLink自动关联生命周期这是一个极其方便的功能它能将Tween的生命周期与一个GameObject绑定。当该GameObject被销毁时Tween会自动被终止。transform.DOMoveX(10, 2f) .SetLink(gameObject); // 将此动画链接到当前游戏物体现在你不需要在OnDestroy中手动调用DOKill()了。这在管理大量动态生成的物体如弹幕、特效时能有效防止内存泄漏和错误。4.2 时间控制与独立时间轴Unity的Time.timeScale会影响所有基于Time.deltaTime的动画包括DOTween的默认动画。当你需要实现游戏暂停时这很方便。但有时你希望UI动画不受游戏暂停影响或者为某个特定物体创建慢动作效果。// 创建一个独立于Unity Time.timeScale的时间轴 Tween myTween transform.DOMoveX(10, 2f) .SetUpdate(UpdateType.Normal, true); // 第二个参数为true表示使用独立时间不受Time.timeScale影响 // 你甚至可以自定义这个独立时间的缩放 DOTween.timeScale 0.5f; // 所有DOTween动画默认类型变慢一半 myTween.timeScale 2f; // 仅这个特定的动画加速一倍在制作UI菜单弹出动画时我总会加上SetUpdate(true)确保即使游戏逻辑暂停菜单的打开关闭动画依然流畅。4.3 路径动画与DOTweenPath组件Pro版核心这是DOTween Pro的杀手锏。假设你要做一个飞机巡逻的路径。添加DOTweenPath组件到你的飞机GameObject上。在Scene视图中点击组件上的“Add Waypoint”按钮然后在场景中点击添加路径点。你可以自由拖动这些点来塑造曲线。在Inspector中配置参数Path Type 选择CatmullRom平滑曲线或Linear直线。Duration 走完全程的时间。Ease 路径运动的缓动类型。Loop Type 循环方式。Orientation 物体的朝向可以设置为沿路径切线方向这样飞机会自然“转弯”。勾选Play On Awake运行游戏飞机就会自动沿路径飞行。代码控制DOTweenPathDOTweenPath path GetComponentDOTweenPath(); path.DOPlay(); // 播放 path.DOPause(); // 暂停 path.DOTogglePause(); // 切换暂停/播放 path.DORestart(); // 重新开始可视化编辑加代码控制实现了完美的设计期与运行期分离。策划可以在编辑器里随意拖拽路径程序员只需调用简单的播放控制接口。4.4 可视化编辑实战Pro版核心对于UI动画序列Visual Editor的优势巨大。例如制作一个弹窗的入场动画选中弹窗的根Canvas。在Inspector中点击“Add Tween” - “CanvasGroup” - “Alpha”。设置From为0To为1Duration为0.3秒Ease为OutBack。这是淡入。再次点击“Add Tween” - “RectTransform” - “Local Scale”。设置From为Vector3(0.8, 0.8, 1)To为Vector3.oneDuration为0.4秒Ease为OutElastic。这是缩放弹入。在Visual Editor的列表里你可以拖动这两个Tween来调整它们的相对顺序和延迟时间形成一个先缩放后淡入的复合效果。所有这些配置都作为组件数据保存下来。程序员只需要在代码中获取这个DOTweenAnimation组件调用DOPlay()或DORestart()即可。这极大地降低了动画迭代的成本也使得非技术人员能够直接参与内容创作。5. 常见问题排查与性能调优指南5.1 典型问题速查表问题现象可能原因解决方案动画完全不执行1. DOTween未初始化。2. Tween被立即Kill或Pause了。3. 目标对象为null安全模式下会静默失败。1. 确保在脚本执行前调用DOTween.Init()。2. 检查代码逻辑确认没有在创建后立即调用Kill()或Pause()。3. 检查动画目标引用是否有效。动画卡顿、不流畅1. 同一帧创建了过多Tween。2.OnUpdate回调中执行了耗时操作。3. 未开启Tween回收导致GC频繁触发。1. 错峰创建动画或使用SetDelay分散。2. 优化OnUpdate回调避免复杂计算和Find、GetComponent等操作。3. 初始化时设置recycleAllByDefault: true。WebGL平台动画异常1. 代码执行顺序问题DOTween初始化晚于动画创建。2. WebGL的单线程模式与某些协程或线程操作冲突。1. 使用[RuntimeInitializeOnLoadMethod]特性确保最早初始化。2. 避免在Tween回调中执行可能导致主线程阻塞的操作。简化WebGL端的动画逻辑。材质动画DOColor/DOFloat无效1. 材质属性名称拼写错误区分大小写。2. 材质不是可写的如从Renderer.sharedMaterial获取。3. Shader中不存在该属性。1. 检查Shader中属性的确切名称通常以_开头。2. 使用Renderer.material获取实例材质但注意这会创建材质副本有性能开销。3. 确认Shader支持该属性动画。UI动画位置错乱使用了DOMove而非DOAnchorPos。DOMove操作世界坐标而UI系统基于锚点和轴心。对RectTransform进行位置动画务必使用DOAnchorPos或DOAnchorPos3D。动画结束后物体状态恢复默认情况下某些Tween如DOFade在循环或完成后会恢复初始值。如果希望保持动画结束时的状态使用.SetLoops(-1, LoopType.Yoyo)或.From()方法或者在OnComplete中手动设置最终状态。5.2 性能深度调优策略1. 监控与统计DOTween提供了静态方法DOTween.TotalPlayingTweens来获取当前正在播放的Tween数量。在性能关键阶段如战斗场景可以监控这个数字。如果数量异常多例如超过几百个就需要审视动画设计是否合理。2. 批量操作与SetAutoKill(false)对于大量相同物体的相同动画比如一堆金币同时飞向UI目标不要为每个物体单独创建并管理Tween。可以创建一个模板Tween然后用SetAutoKill(false)和Pause()缓存起来对每个物体应用时使用tween.ChangeValues(newTarget, newEndValue)来改变目标值和结束值然后Restart()。这比创建上百个新Tween对象高效得多。3. 慎用OnUpdateOnUpdate每帧调用是性能黑洞。如果确实需要在动画过程中同步某些状态考虑是否可以改用OnStepComplete或在动画序列中插入回调。如果必须用确保回调函数内的代码是轻量级的。4. 平台差异化处理在移动端可以适当降低动画的“流畅度”。例如减少同时播放的粒子特效相关的颜色/缩放动画数量。对于非核心的、屏幕外的UI动画可以考虑降低其更新频率虽然DOTween本身不直接支持但你可以通过控制其父物体的激活状态来间接实现。5.3 与Unity其他系统的协作1. 与UI系统如Button的交互在为按钮添加点击动画时要注意Unity UI的事件阻塞。一个常见的做法是在按钮的OnClick事件中先播放一个简短的缩放或颜色脉冲动画DOPunchScale/DOColor然后在动画的OnComplete回调中再执行真正的业务逻辑。这能防止动画播放期间用户快速连续点击触发多次逻辑。2. 与协程Coroutine的配合你可以用yield return myTween.WaitForCompletion();在协程中等待一个动画播放完毕。这比用yield return new WaitForSeconds(duration)更精确因为它等待的是动画实际结束而非固定的时间。3. 与Timeline或Animator的取舍DOTween擅长的是程序化、动态生成的动画。对于复杂的、预先定义好的、需要与音频或剧情紧密同步的角色骨骼动画或过场动画Unity的Animator和Timeline是更专业的选择。DOTween的定位是“代码驱动的即时动画”两者可以互补。例如用Timeline控制镜头和角色的大段演出用DOTween实时控制场景中动态生成的光效强度或UI元素的反馈。6. 实战案例一个复杂的UI弹窗动画序列让我们综合运用以上知识实现一个带有背景遮罩、弹性弹出、内容渐入的弹窗。using DG.Tweening; using UnityEngine; using UnityEngine.UI; public class ComplexPopup : MonoBehaviour { public CanvasGroup backgroundMask; // 半透明黑色背景遮罩 public RectTransform popupPanel; // 弹窗面板 public CanvasGroup contentGroup; // 弹窗内部内容的CanvasGroup用于整体淡入 public float animationDuration 0.4f; private Sequence _showSequence; private Sequence _hideSequence; void Awake() { // 初始化时隐藏所有元素 backgroundMask.alpha 0; backgroundMask.gameObject.SetActive(false); // 先禁用节省性能 popupPanel.localScale Vector3.zero; contentGroup.alpha 0; } public void ShowPopup() { // 如果已经在显示则先杀死旧动画 _hideSequence?.Kill(); _showSequence?.Kill(); // 激活背景 backgroundMask.gameObject.SetActive(true); // 创建显示序列 _showSequence DOTween.Sequence(); // 步骤1背景淡入 _showSequence.Append(backgroundMask.DOFade(0.7f, animationDuration * 0.5f).From(0)); // 步骤2面板弹性弹出同时进行 _showSequence.Join(popupPanel.DOScale(1f, animationDuration * 0.8f) .SetEase(Ease.OutBack, 1.2f) // OutBack带一点过冲弹性1.2是过冲幅度 .From(Vector3.zero)); // 步骤3面板弹出后内容渐入稍晚一点开始 _showSequence.AppendInterval(animationDuration * 0.3f); // 加入一点点延迟 _showSequence.Append(contentGroup.DOFade(1f, animationDuration * 0.4f).From(0)); // 设置整个序列不自动杀死方便重用 _showSequence.SetAutoKill(false); // 播放 _showSequence.Restart(); } public void HidePopup() { // 如果正在显示先杀死显示动画 _showSequence?.Kill(); // 创建隐藏序列 _hideSequence DOTween.Sequence(); // 步骤1内容先淡出 _hideSequence.Append(contentGroup.DOFade(0, animationDuration * 0.3f)); // 步骤2面板缩小消失 _hideSequence.Join(popupPanel.DOScale(0f, animationDuration * 0.5f) .SetEase(Ease.InBack)); // InBack 提供一点向内收缩的感觉 // 步骤3最后背景淡出 _hideSequence.Append(backgroundMask.DOFade(0, animationDuration * 0.4f)); // 动画完成后禁用背景对象以完全隐藏 _hideSequence.OnComplete(() { backgroundMask.gameObject.SetActive(false); // 可选将面板缩放重置为下次显示做准备 popupPanel.localScale Vector3.zero; contentGroup.alpha 0; }); // 播放 _hideSequence.Play(); } void OnDestroy() { // 清理动画防止内存泄漏 _showSequence?.Kill(); _hideSequence?.Kill(); // 也可以使用 SetLink(this) 来简化生命周期管理 } }这个案例的要点序列编排 使用Sequence精确控制背景、面板、内容三者动画的先后和并行关系。缓动选择OutBack用于弹出创造惊喜感InBack用于收起增加质感。性能考虑 动画播放完毕后才禁用背景GameObject避免中间状态穿帮。使用SetAutoKill(false)和缓存Sequence引用避免重复创建。生命周期 在OnDestroy中清理动画并使用SetLink是更简洁的替代方案。7. 进阶自定义插件与扩展DOTweenDOTween的架构是开放的允许你为自己定义的类或第三方库创建扩展方法。例如如果你有一个自定义的HealthBar组件希望平滑地改变血量值。using DG.Tweening; using DG.Tweening.Core; using DG.Tweening.Plugins.Core; using DG.Tweening.Plugins.Options; public static class DOTweenExtensions { // 为HealthBar类创建一个DOMyCustomValue扩展方法 public static TweenerCorefloat, float, FloatOptions DOMyCustomValue(this HealthBar target, float endValue, float duration) { // 创建一个Tweener它会在duration时间内将target.MyValue从当前值渐变到endValue return DOTween.To( () target.MyValue, // 获取器获取当前值 x target.MyValue x, // 设置器设置新值 endValue, // 目标值 duration // 持续时间 ).SetTarget(target); // 将Tween与目标关联便于管理 } } // 使用的时候就像使用内置方法一样简单 HealthBar myHealthBar GetComponentHealthBar(); myHealthBar.DOMyCustomValue(0.5f, 1f).SetEase(Ease.OutQuad);这个例子展示了DOTween的核心DOTween.To方法它是最通用的创建补间动画的方式。通过自定义扩展你可以将DOTween的流畅动画能力赋能给项目中的任何数值属性。在我个人的项目经验里DOTween早已超越了“一个动画插件”的范畴它更像是一种思维模式鼓励开发者用声明式、组合式的方式来思考动态交互。它解决的不是“能不能动”的问题而是“如何动得优雅、高效且易于维护”的问题。无论是简单的反馈提示还是复杂的剧情演出当你习惯用DOTween.Sequence()来编排逻辑时代码的清晰度和表现力都会上一个台阶。最后一个小建议多花点时间在Ease枚举上尝试不同的缓动函数细微的曲线变化带来的手感提升往往是区分普通动画和优秀动画的关键。
返回列表