ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DOTween回调机制全解析:从OnComplete到OnWaypointChange的实战指南

2026/8/12 22:55:36 拓冰建站 浏览量
DOTween回调机制全解析:从OnComplete到OnWaypointChange的实战指南

1. 项目概述:为什么DOTween的回调值得你花时间研究?

如果你在Unity项目里用过DOTween,那你肯定对.OnComplete(()=>{})这行代码不陌生。它几乎是每个动画收尾时的标配。但当我看到很多新手,甚至一些有经验的开发者,还在为“为什么我的回调没触发?”、“OnWaypointChange怎么用?”这类问题头疼时,我觉得有必要把这块“硬骨头”彻底啃下来,分享给大家。

DOTween的回调系统,远不止一个OnComplete。它是一套精细的事件触发器,从动画开始、更新、到暂停、重启、结束乃至被销毁,每一个关键节点都为你预留了钩子。理解它们,不仅能让你写出更健壮、更易维护的动画逻辑,更能帮你避开许多隐形的“坑”——比如在对象被销毁后回调导致的空引用,或者因为循环模式设置不当导致回调触发时机错乱。

这篇文章,我会以一个踩过无数坑的“老Tweener”的身份,带你从最基础的OnComplete开始,一路深入到OnWaypointChangeOnPlayOnKill等所有核心回调。我会用大量实际代码示例,解释每个回调的触发时机、应用场景,以及那些官方文档里没写的、但在实战中至关重要的“潜规则”和避坑技巧。无论你是刚接触DOTween的新手,还是想深化理解的中级开发者,这篇指南都能让你对DOTween的回调机制有一个通透的掌握。

2. DOTween回调函数全景图与核心设计逻辑

在深入每个回调之前,我们得先有一张“地图”。DOTween的回调不是随意堆砌的,它们遵循着动画生命周期的清晰脉络。理解这个设计逻辑,比死记硬背每个回调的名字更重要。

2.1 回调函数的分类与生命周期

我们可以把DOTween的回调大致分为三类,它们分别对应动画状态机的不同阶段:

  1. 状态变更回调:在动画状态发生改变时触发。这是最常用的一类。

    • OnPlay/OnStart: 动画开始播放时。
    • OnPause: 动画被暂停时。
    • OnRewind: 动画被倒带(Rewind)时。
    • OnUpdate: 动画在每一帧更新数值时。
    • OnStepComplete: 完成一个“步进”时(对于循环动画,指完成单次循环)。
    • OnComplete: 动画全部完成时(包括所有循环)。
    • OnKill: 动画被销毁(Kill)时。
  2. 路径与路径点回调:专门用于处理DOPathDOLocalPath等路径动画。

    • OnWaypointChange: 当动画移动到路径上的一个新路点时触发。
  3. 参数化回调:允许你向回调函数传递额外参数,这在泛化处理时非常有用。

    • OnCompleteOnStepComplete等大多数回调都支持一个带参数的版本,例如.OnComplete(MyCallback, myParam)

一个核心心法:DOTween的Tween对象(TweenerSequence)是一个状态机。它的状态包括PlayingPausedRewindedCompleted等。回调就是在这些状态转换的“边缘”被触发的。当你调用Play()Pause()Restart()Kill()这些方法时,你就是在驱动这个状态机,从而可能触发相应的回调。

注意:这里有一个新手极易混淆的点:OnStartOnPlay。在DOTween中,OnStart是一个更底层的概念,它只在Tween第一次开始播放时触发一次(即使你之后暂停再播放也不会再触发)。而OnPlay则在每次调用Play()Restart()时都会触发。对于大多数“开始动画时执行某操作”的需求,使用OnPlay更符合直觉。

2.2 回调的注册方式与链式调用

DOTween的API设计非常优雅,支持流畅的链式调用(Fluent Interface)。这意味着你可以在定义动画的同时,一气呵成地注册所有回调。

// 一个典型的链式调用示例 transform.DOMoveX(10, 2f) // 创建一个移动动画 .SetEase(Ease.OutCubic) // 设置缓动曲线 .OnStart(() => Debug.Log("动画开始!")) // 注册开始回调 .OnUpdate(() => Debug.Log($"当前位置: {transform.position.x}")) // 注册更新回调 .OnComplete(() => Debug.Log("动画完成!")) // 注册完成回调 .SetLoops(3, LoopType.Yoyo); // 设置循环

这种写法的优势在于清晰和集中。所有关于这个动画的定义(目标、时长、缓动、循环、回调)都写在一起,便于阅读和维护。你不需要先把Tween存到一个变量里,再分开去设置回调。

但是,链式调用也有一个“坑”:如果你需要复用同一个回调函数,或者在动画创建后才动态添加回调,链式调用就不太方便了。这时,你可以将Tween保存到一个变量中,然后像调用方法一样添加回调。

Tweener myTween = transform.DOMoveX(10, 2f); // ... 其他地方,根据条件动态添加回调 if (someCondition) { myTween.OnComplete(HandleSpecialCompletion); } else { myTween.OnComplete(HandleNormalCompletion); }

3. 核心回调函数深度解析与避坑实战

现在,我们进入重头戏,逐一拆解每个核心回调。我会用“是什么”、“何时触发”、“怎么用”、“注意什么”这个结构来讲解,并附上代码和常见问题。

3.1 OnComplete:你以为的完成,并不是真正的完成

OnComplete是最常用的回调,没有之一。它的定义很简单:在动画自然结束时触发。

关键解读:“自然结束”这意味着:

  1. 动画播放到了它定义的终点(对于非循环动画,就是时长结束;对于循环动画,是完成了所有循环次数)。
  2. 动画没有被中途Kill()
  3. 动画没有被设置为SetAutoKill(false)且处于无限循环状态(永远不会自然结束)。
// 示例1:基本用法 transform.DOMove(Vector3.one, 1f) .OnComplete(() => { Debug.Log("移动动画完成!"); // 可以在这里触发下一个动画,或者更新游戏状态 gameObject.SetActive(false); }); // 示例2:带参数的OnComplete void ProcessCompletion(string message) { Debug.Log($"动画完成,消息: {message}"); } transform.DORotate(new Vector3(0, 360, 0), 2f, RotateMode.LocalAxisAdd) .OnComplete(ProcessCompletion, "旋转一圈完毕!");

避坑指南1:循环动画与OnComplete这是新手第一个大坑。如果你设置了循环,OnComplete会在所有循环都结束后才触发。

// 这个OnComplete会在3次Yoyo循环全部完成后才触发一次。 transform.DOScale(Vector3.one * 2, 0.5f) .SetLoops(3, LoopType.Yoyo) .OnComplete(() => Debug.Log("循环全部完成"));

如果你希望在每次循环结束时都做点事情,应该用OnStepComplete

避坑指南2:OnComplete 与 Kill()如果你在动画播放中途手动调用了Tween.Kill()DOTween.Kill(transform)OnComplete不会触发。因为Kill是强制立即终止动画,不属于“自然结束”。如果你需要在动画被销毁时进行清理,应该使用OnKill回调。

避坑指南3:空引用(Null Reference)这是Unity开发中的经典问题,在回调中尤其致命。如果你的回调函数里访问了某个GameObject或Component,而这个对象在回调触发前被销毁了,就会抛出MissingReferenceException

// 危险代码! public class DangerousScript : MonoBehaviour { public Transform target; void Start() { // 假设动画持续5秒 transform.DOMove(Vector3.up, 5f) .OnComplete(() => { // 如果5秒内 `target` 或 `target.gameObject` 被Destroy了,这里就会崩溃! target.position = Vector3.zero; }); } }

解决方案:在回调内部进行空值检查。

.OnComplete(() => { // 检查对象和组件是否仍然有效 if (target != null && target.gameObject != null) { target.position = Vector3.zero; } else { Debug.LogWarning("目标对象已销毁,跳过回调操作。"); } })

更优雅的解决方案是使用SetLink方法,将Tween的生命周期与GameObject绑定,当GameObject销毁时自动Kill掉Tween,但这也无法完全避免在Kill前回调被触发的问题,所以空值检查是最终防线。

3.2 OnStepComplete:循环动画中的“节拍器”

OnStepComplete是理解DOTween循环机制的关键。它在一个“步骤”完成时触发。对于非循环动画,一个“步骤”就是整个动画。对于循环动画,每次循环结束就是一个“步骤”。

// 示例:用OnStepComplete制作一个闪烁3次的特效 private int blinkCount = 0; image.DOFade(0, 0.2f) // 从当前透明度淡出到0 .SetLoops(6, LoopType.Yoyo) // 3次完整闪烁(Yoyo一次包含淡出和淡入) .OnStepComplete(() => { blinkCount++; Debug.Log($"完成第 {blinkCount} 次闪烁"); // 可以在每次闪烁后更新UI计数等 }) .OnComplete(() => Debug.Log("闪烁特效全部结束"));

在这个例子里,SetLoops(6, LoopType.Yoyo)会让动画在0和1之间往返6次(即3个完整的“淡出-淡入”周期)。OnStepComplete会被触发6次,而OnComplete只在最后触发1次。

避坑指南:Restart() 与 OnStepComplete当你对一个循环动画调用Restart()时,它的内部循环计数会被重置。但需要注意的是,Restart()会立即触发一次OnPlay,但不会触发OnStepComplete,因为它还没有完成任何一个“步骤”。

3.3 OnWaypointChange:掌控路径动画的每一个拐点

这是路径动画的专属回调,极其强大。当你使用DOPath让物体沿一组路径点移动时,OnWaypointChange会在物体即将前往下一个路径点时触发。

public Vector3[] waypoints = new Vector3[] { new Vector3(0, 0, 0), new Vector3(5, 2, 0), new Vector3(10, 0, 0), new Vector3(5, -2, 0), new Vector3(0, 0, 0) // 回到起点 }; void Start() { transform.DOPath(waypoints, 5f, PathType.CatmullRom) // 使用CatmullRom曲线让路径更平滑 .SetLookAt(0.01f) // 让物体朝向移动方向(参数是向前看的前置量) .OnWaypointChange(OnWaypointReached) // 注册路径点变更回调 .SetOptions(true); // 设置PathOptions,例如是否闭合路径 } void OnWaypointReached(int waypointIndex) { // waypointIndex 是即将前往的**新**路径点的索引 Debug.Log($"正在前往路径点 {waypointIndex} (坐标: {waypoints[waypointIndex]})"); // 实战应用:在特定路径点触发事件 switch(waypointIndex) { case 1: // 到达第一个拐点,播放音效 audioSource.PlayOneShot(turnSound); break; case 3: // 到达第三个拐点,生成一个特效 Instantiate(sparkEffect, transform.position, Quaternion.identity); break; } }

核心要点与避坑

  • 触发时机:回调在离开当前点,前往下一个点的时刻触发。参数waypointIndex目标点的索引,不是当前点。
  • 索引范围:对于有N个点的路径,索引从0到N-1。当从最后一个点回到起点(如果是闭合路径)或结束时,索引会再次变为0或触发完成逻辑。
  • 与LookAt结合SetLookAt常与路径动画一起使用,让物体平滑转向。但注意,OnWaypointChange触发时,物体的旋转可能还在追赶移动方向。如果你需要物体在完全对准新方向后再做某事,可能需要结合OnUpdate或在路径点设置一个小的延迟(用Sequence插入空档)。
  • 性能:路径点很多且动画很快时,OnWaypointChange会每帧触发多次。确保你的回调函数是轻量级的,避免在这里做复杂的计算或实例化大量对象。

3.4 OnUpdate:每一帧的监控与干预

OnUpdate在动画的每一帧(更准确地说,是每次Tween被更新时)都会被调用。这是你介入动画过程、实现自定义效果的强大工具。

public Light pointLight; private float baseIntensity; void Start() { baseIntensity = pointLight.intensity; transform.DOMoveX(20, 3f) .OnUpdate(() => { // 根据移动距离动态改变灯光强度 float distanceRatio = transform.position.x / 20f; pointLight.intensity = baseIntensity + Mathf.Sin(distanceRatio * Mathf.PI) * 2f; // 另一个常见用途:更新UI进度条 // progressBar.fillAmount = distanceRatio; }) .OnComplete(() => pointLight.intensity = baseIntensity); // 动画结束后恢复 }

避坑指南:性能与垃圾回收OnUpdate每帧都会调用,如果回调函数创建了新的对象(比如new Vector3()、字符串连接等),就会产生垃圾,引发GC(垃圾回收),可能导致卡顿。

// 糟糕的写法:每帧都创建新的字符串 .OnUpdate(() => { Debug.Log($"位置是: {transform.position}"); // transform.position.ToString() 会产生垃圾 }); // 改进的写法:控制日志频率或使用StringBuilder(在复杂情况下) private float logTimer = 0f; .OnUpdate(() => { logTimer += Time.deltaTime; if (logTimer > 0.5f) { // 每0.5秒打印一次 Debug.Log($"位置是: {transform.position.x:F2}, {transform.position.y:F2}"); // 格式化减少精度 logTimer = 0f; } });

3.5 OnPlay, OnPause, OnRewind:状态监控三剑客

这三个回调用于响应动画的播放控制状态变化。

  • OnPlay: 当动画开始播放时触发。调用Play()Restart()PlayForward()等方法都会触发。对于Sequence,当序列开始播放时也会触发。
  • OnPause: 当动画被暂停时触发。调用Pause()方法触发。
  • OnRewind: 当动画被倒带时触发。调用Rewind()方法触发。注意,Restart()会先触发OnRewind(因为要回到开头),再触发OnPlay
// 示例:用UI按钮控制动画,并同步按钮状态 public UnityEngine.UI.Button playButton; public UnityEngine.UI.Button pauseButton; void Start() { Tweener tween = transform.DOScale(2, 2f).SetAutoKill(false).Pause(); // 创建后暂停 tween.OnPlay(() => { playButton.interactable = false; pauseButton.interactable = true; Debug.Log("动画开始播放"); }); tween.OnPause(() => { playButton.interactable = true; pauseButton.interactable = false; Debug.Log("动画已暂停"); }); playButton.onClick.AddListener(() => tween.Play()); pauseButton.onClick.AddListener(() => tween.Pause()); }

避坑指南:OnStart 的独特性再次强调OnStart,它只在Tween整个生命周期中第一次开始播放时触发一次。即使你暂停后再播放,它也不会再触发。如果你需要每次播放都执行初始化,请用OnPlay

3.6 OnKill:最后的清理机会

OnKill在Tween被销毁时触发。触发方式包括:动画自然结束且SetAutoKill(true)(默认)、手动调用Kill()、Tween关联的GameObject被销毁且使用了SetLink

private Tweener _activeTween; void StartAnimation() { // 如果已有动画在运行,先安全地终止它 if (_activeTween != null && _activeTween.IsActive()) { _activeTween.Kill(); // 这会触发旧Tween的OnKill _activeTween = null; } _activeTween = transform.DOMoveX(10, 2f) .OnKill(() => { Debug.Log("动画被销毁,进行资源清理..."); // 例如,将_activeTween引用置空(虽然上面已经做了,但这里是双重保险) // 或者释放一些非托管资源 _activeTween = null; }) .OnComplete(() => { Debug.Log("动画自然完成"); _activeTween = null; // 自然完成后,Tween会被AutoKill,引用也应置空 }); }

最重要的避坑指南:OnKill vs OnComplete这是必须理清的关系:

  • 自然完成:先触发OnComplete,然后因为AutoKill,再触发OnKill
  • 手动Kill:触发OnKill不触发OnComplete
  • 永远记住:在OnKill回调里,不要再访问或操作这个Tween本身,因为它正在被销毁。你只能做一些外部资源的清理工作。

4. 高阶应用与组合拳:用回调构建复杂动画逻辑

掌握了单个回调后,我们可以将它们组合起来,解决更复杂的动画编排问题。

4.1 使用Sequence编排带回调的动画序列

Sequence(序列)是DOTween中用于编排多个动画顺序播放或并行播放的容器。回调可以挂在Sequence本身,也可以挂在它包含的每个Tween上。

Sequence attackSequence = DOTween.Sequence(); // 第一段:冲锋 attackSequence.Append(transform.DOMove(enemyPosition, 0.5f).SetEase(Ease.OutQuad)); // 在冲锋结束时(即第一段动画的OnComplete时机)插入一个回调 attackSequence.AppendCallback(() => { Debug.Log("冲锋结束,开始攻击!"); PlaySwordSwingEffect(); }); // 第二段:攻击动作(旋转) attackSequence.Append(transform.DORotate(new Vector3(0, 360, 0), 0.3f, RotateMode.LocalAxisAdd)); // 第三段:后跳 attackSequence.Append(transform.DOMove(originalPosition, 0.4f).SetEase(Ease.InQuad)); // 整个序列完成后的回调 attackSequence.OnComplete(() => { Debug.Log("连招结束"); characterState = State.Idle; }); attackSequence.Play();

技巧AppendCallbackInsertCallback让你能在序列的精确时间点插入无动画的“空档”来执行逻辑,这比在单个Tween的OnComplete里处理要清晰得多,尤其是当多个Tween并行时。

4.2 利用OnUpdate实现动画驱动的事件系统

你可以用OnUpdate来模拟一个简单的动画事件轨道。

public class AnimationEventDispatcher : MonoBehaviour { public System.Action<float> onProgressUpdated; // 进度更新事件 public System.Action<int> onMarkerReached; // 标记点到达事件 private float[] eventMarkers = {0.3f, 0.6f, 0.9f}; // 在30%,60%,90%进度时触发 private int currentMarkerIndex = 0; void PlayAnimation() { currentMarkerIndex = 0; transform.DOScale(3, 4f) .OnUpdate(() => { // 获取当前动画进度(0到1) float progress = DOVirtual.EasedValue(0, 1, ???, Ease.Linear); // 注意:上面这行是概念代码,实际需要通过Tween的完整时间和已用时间计算 // 更简单的方式是使用DOVirtual.DelayedCall或直接基于Time.deltaTime计算 // 这里我们用一种更直接的方法:基于时间的模拟 // 实际项目中,你可能需要自己记录开始时间来计算精确进度 // 触发进度更新 onProgressUpdated?.Invoke(progress); // 检查标记点 if (currentMarkerIndex < eventMarkers.Length && progress >= eventMarkers[currentMarkerIndex]) { onMarkerReached?.Invoke(currentMarkerIndex); currentMarkerIndex++; } }); } } // 使用方式 // dispatcher.onMarkerReached += (index) => { if(index == 0) PlaySound(); };

4.3 回调与对象生命周期的协同管理

这是确保项目稳定性的高级话题。核心原则是:Tween的生命周期必须与其操作的目标对象(GameObject/Component)的生命周期保持一致

推荐模式:使用SetLinkSetLink是DOTween提供的最佳实践工具。它将Tween与一个GameObject链接起来,当该GameObject被销毁时,自动Kill这个Tween。

void Start() { // 将此Tween与this.gameObject链接 transform.DOMoveX(10, 2f) .SetLink(gameObject) // 关键行! .OnComplete(() => { if (this != null) { // SetLink后,这个检查通常更安全,但非绝对 // 执行完成逻辑 } }); }

SetLink有几种模式(LinkBehaviour):

  • KillOnDestroy(默认):链接对象销毁时Kill Tween。
  • KillOnDisable:链接对象禁用(SetActive(false))时Kill Tween。
  • PauseOnDisable/PauseOnDestroy:暂停而非杀死。
  • PlayOnEnable/RestartOnEnable:与禁用/启用配合使用。

在UI动画或频繁启用/禁用的对象上,正确使用SetLink的模式可以避免很多诡异的问题。

手动管理模式对于需要更精细控制的场景(比如对象池),你需要手动管理。

public class PooledObject : MonoBehaviour { private List<Tween> _activeTweens = new List<Tween>(); public void AnimateIn() { var tween = transform.DOScale(1, 0.5f).From(0).SetAutoKill(false); _activeTweens.Add(tween); tween.Play(); } public void ReturnToPool() { // 在对象回池前,清理所有关联的动画 foreach (var t in _activeTweens) { if (t != null && t.IsActive()) { t.Kill(); // 触发OnKill } } _activeTweens.Clear(); // ... 其他回池逻辑 } }

5. 常见问题排查与调试技巧实录

即使理解了原理,实战中还是会遇到各种问题。下面是我总结的“排错清单”。

5.1 问题一:回调函数根本没触发

可能原因及排查步骤:

  1. 动画真的开始了吗?检查你是否调用了Play()。使用DOTween.ToDO系列方法创建的Tween默认是**暂停(Paused)**状态的(除非你使用了SetDelay且参数小于0,它会自动播放)。很多人创建了Tween就以为它在运行。

    • 解决:在链式调用的最后加上.Play(),或者使用.SetAutoPlay(true)
  2. 动画被Kill了吗?如果动画在触发回调前就被Kill()了,OnComplete等回调自然不会触发。检查是否有其他地方(比如另一个脚本、OnDisable方法里)调用了Kill

    • 调试:在可能Kill的地方加日志,或者给Tween设置一个ID,用DOTween.Kill(id)来精确控制。
  3. 循环模式的影响?如果你设置了无限循环(SetLoops(-1)),那么OnComplete永远等不到“自然结束”。

    • 解决:对于无限循环,用OnStepComplete来响应每次循环结束,或者用OnUpdate
  4. 作用域问题导致回调被GC?如果你将回调函数写在一个局部方法中,并且没有保持对Tween的引用,Tween可能在某个时刻被垃圾回收器(GC)标记,导致行为异常(虽然DOTween内部有机制防止此情况,但复杂情况下仍可能发生)。

    • 解决:将Tween保存在类的成员变量中,确保其生命周期可控。

5.2 问题二:回调触发的时机不对(太早、太晚或多次触发)

  1. 太早触发:最常见于OnComplete在循环动画中。你误以为它会在每次循环完触发,其实它只在所有循环结束后触发。改用OnStepComplete

  2. 太晚触发或感觉有延迟:检查是否在回调里做了耗时操作(如加载资源、复杂计算)。Unity的主线程是单线程的,耗时的回调会阻塞动画更新乃至整个游戏帧。考虑使用协程(Coroutine)或异步任务(async/await)将耗时操作移出回调。

  3. 多次触发

    • 重复注册:你是否在每帧的Update里都创建了新的Tween并添加了回调?确保动画创建逻辑只执行一次。
    • Sequence嵌套:如果Sequence中的某个Tween也有自己的OnComplete,而Sequence本身也有OnComplete,它们都会触发。理清你需要的层级。
    • Kill和Restart:对一个Tween调用Restart(),它会重新播放,并再次触发OnPlay,但之前注册的回调依然有效。如果你不想要旧的回调,在Restart前先Kill再新建,或者使用OnRewindOnPlay来管理状态。

5.3 问题三:在回调中修改动画属性导致意外行为

你想在OnUpdate里根据条件提前结束动画,于是你写了:

.OnUpdate(() => { if (shouldStop) { transform.DOKill(); // 危险! // 或者 myTween.Kill(); } })

问题:在OnUpdate内部Kill当前正在更新的Tween,可能会干扰DOTween的内部更新循环,导致不可预知的行为,甚至崩溃。

安全做法:设置一个标志位,在OnUpdate外(比如下一帧)执行Kill。

private bool _killFlag = false; private Tweener _myTween; void Start() { _myTween = transform.DOMoveX(10, 5f) .OnUpdate(() => { if (Input.GetKeyDown(KeyCode.Space)) { _killFlag = true; } }); } void Update() { if (_killFlag) { _killFlag = false; _myTween.Kill(); Debug.Log("动画已安全终止"); } }

或者,更优雅地,使用DOTween.To并提供自己的setter,在setter里判断条件并返回新值,但这更适合控制动画值而非中断。

5.4 调试技巧:给Tween加上“身份证”

当场景中有大量Tween时,很难知道是哪个触发了回调。使用SetId给Tween设置一个字符串ID,在回调中打印出来。

transform.DOMoveX(10, 2f) .SetId("Player_Run_Animation") // 设置ID .OnComplete(() => Debug.Log($"Tween completed: {this.Id}")); // 在回调中访问Id属性 // 你也可以通过ID来控制一组Tween DOTween.Play("Player_Run_Animation"); DOTween.Pause("Player_Run_Animation"); DOTween.Kill("Player_Run_Animation");

5.5 性能监控:发现隐藏的回调开销

在Profiler中,频繁的、尤其是每帧执行的OnUpdate回调是性能热点。确保:

  1. 回调函数本身执行效率高。
  2. 避免在OnUpdate中执行FindGetComponent、实例化对象等昂贵操作。
  3. 考虑是否需要每帧都触发。也许你可以用DOVirtual.DelayedCall来间隔执行。

最后,分享一个我个人的调试习惯:在开发复杂动画逻辑时,我会创建一个简单的“Tween调试器”单例,它记录所有活跃Tween的ID、目标、类型和状态,并在屏幕一角显示。当回调行为异常时,看一眼这个列表,往往能立刻发现问题所在。这比在代码里到处加Debug.Log要高效得多。实现起来也不难,利用DOTween的静态方法DOTween.TweensByIdDOTween.TweensByTarget等就可以获取到活跃的Tween信息。