DOTween动画:TweenManager深度解析

TweenManager 是 DOTween 动画引擎最核心的数据中枢与底层调度器

TweenManager 是控制所有动画生命周期、内存池、帧更新与批量操作的"大脑"。它采用全静态架构,通过原生数组与自定义算法实现了极致的 零 GC(垃圾回收) 和高效的内存复用。

以下是对 TweenManager 源码的核心机制与架构设计的深度拆解:

一、 核心数据结构与内存管理

TweenManager 放弃了大部分 C# 动态容器(如 List<T>),转而采用原生固定长度数组来管理活动与池化的动画对象,避免频繁扩容带来的内存碎片

  1. _activeTweens (Tween[]):存储当前所有正在运行的 Tween 的主数组,初始容量 250。

  2. _pooledTweeners (Tween[]):Tweener 对象池数组。

  3. _PooledSequences (Stack<Tween>):Sequence 容器池(采用栈存储)。

  4. _KillList (List<Tween>) :专门用于在帧更新循环末尾进行批量延迟销毁的缓存列表。

  5. _TweenLinks (Dictionary<Tween, TweenLink>) :存储通过 .SetLink() 与 Unity GameObject 绑定的生命周期映射表。

二、 对象池与类型匹配机制 (GetTweener & GetSequence)

为了避免运行时产生 GC Alloc,DOTween 在创建动画时会优先尝试从对象池获取对象。

1. 泛型 Tweener 的精确类型匹配 (GetTweener<T1, T2, TPlugOptions>)

在 C# 中,TweenerCore<Vector3, Vector3, VectorOptions>TweenerCore<float, float, FloatOptions> 是完全不同的类型。

  • 扫描与匹配GetTweener_maxPooledTweenerId 倒序扫描 _pooledTweeners 数组。通过比较 typeofT1typeofT2 以及 typeofTPlugOptions 类型指针,精准取出完全符合泛型签名的空闲 Tweener 实例。

  • 自动扩容 :若池中没有可复用的 Tweener,且总数已达到上限(maxTweeners),系统会自动触发 IncreaseCapacities,按 1.5 倍 动态扩容数组。

2. 对象回收 (Despawn)

当 Tween 被 Kill 或播放完毕时:

  1. 触发 t.onKill 回调。

  2. t.isRecyclable == true,将 Tweener 放回 _pooledTweeners 数组,或将 Sequence 压入 _PooledSequences 栈。如果是 Sequence,还会递归回收其内部包含的所有子 Tween。

  3. 执行 t.Reset() 清空所有状态与回调,等待下次复用。

三、 延迟整理算法(Lazy Array Compaction)

在频繁创建和销毁动画时,如果直接从 _activeTweens 数组中移除元素(即整块向前平移),开销为 O(N)。TweenManager 使用了延迟整理(Lazy Reorganization)策略:

1. 标记置空 (RemoveActiveTween)

  • 当一个 Tween 销毁时,系统仅将其在 _activeTweens 中的位置标记为 null,设置 t.activeId = -1

  • 记录最小断层起始位置 _reorganizeFromId,并标记标志位 _requiresActiveReorganization = true

2. 批量平移整理 (ReorganizeActiveTweens)

在下一帧的 UpdateValidate 或查询接口执行前,若 _requiresActiveReorganizationtrue,系统会触发单次 O(N) 遍历:

复制代码
// 源码逻辑简化:通过双指针一次性将非空元素左移,填补 null 破洞
for (int index = TweenManager._reorganizeFromId + 1; index < num2; ++index) {
    Tween activeTween = TweenManager._activeTweens[index];
    if (activeTween == null) {
        ++num1; // 记录破洞数量
    } else {
        activeTween.activeId = TweenManager._maxActiveLookupId = index - num1;
        TweenManager._activeTweens[index - num1] = activeTween; // 左移覆盖
        TweenManager._activeTweens[index] = null;
    }
}

这种设计将多次删除的平移开销合并为单次批量平移,大幅降低了 CPU 消耗。

四、 帧更新与双阶段销毁管线 (Update)

TweenManager.Update 是驱动动画逐帧演进的核心函数,采用了双阶段处理机制,防止在遍历更新过程中修改数组导致索引异常。

复制代码
    Phase 1: 状态推进 (Update)
┌──────────────────────────────────┐
│ 遍历 _activeTweens                │
│ ├── 评估 TweenLink (GameObject)  │
│ ├── 计算 延迟 (Delay)             │
│ ├── 计算 步进位置 (DoGoto)         │
│ └── 若已结束/失效 ───────┐       │
└──────────────────────────┼───────┘
                           │ MarkForKilling
                           ▼
                 ┌──────────────────┐
                 │    _KillList     │
                 └──────────────────┘
                           │
    Phase 2: 批量回收 (Despawn)
┌──────────────────────────┴───────┐
│ 遍历 _KillList                    │
│ └── 执行 Despawn() & 清空 _KillList│
└──────────────────────────────────┘

第一阶段:更新与步进

  1. 标记 isUpdateLoop = true

  2. 线性遍历 _activeTweens 中对应 UpdateType(Normal、Late、Fixed)的动画。

  3. 调用单体 Update(Tween t, ...)

    • 处理 TweenLink 关联对象的 activeInHierarchy 状态。

    • 计算受 timeScaleindependentTime 修正后的实际增量时间。

    • 处理 Delay 延迟计时。

    • 调用 Tween.DoGoto 计算动画曲线、Ease 缓动与目标值赋权。

第二阶段:批量回收 (DespawnActiveTweens)

  • 当动画播放完毕(且 autoKill == true)或中途被标记杀死时,不会直接从数组内拔除,而是放入 _KillList

  • 主更新循环结束后,集中调用 DespawnActiveTweens(_KillList) 进行统一回收。

五、 统一批量操作接口 (FilteredOperation)

开发者常调用的 API(如 DOTween.KillAll()DOTween.Pause(target)DOTween.Complete(id))在底层全部汇聚到了 FilteredOperation 函数中。

  1. 多维度过滤器 (FilterType)

    • All:作用于全局所有 Active 动画。

    • TargetOrId / TargetAndId:通过 stringint 或对象引用匹配目标(适配 intIdstringIdtarget 属性)。

    • AllExceptTargetsOrIds:排除指定目标后的反向过滤。

  2. 高效逆序遍历 :通过从 _maxActiveLookupId 逆序遍历 _activeTweens,确保在批量 Kill 或 Complete 时能以最快速度完成筛选和状态转换。

六、 GameObject 生命周期联动 (EvaluateTweenLink)

在 DOTween 中,通过 .SetLink(gameObject, LinkBehaviour) 可以将动画与 Unity GameObject 绑定。TweenManager 并没有为每个绑定对象挂载额外的 MonoBehaviour,而是统一通过 EvaluateTweenLink 在帧更新时进行轮询检测:

复制代码
bool activeInHierarchy = tweenLink.target.activeInHierarchy;
bool flag1 = !tweenLink.lastSeenActive & activeInHierarchy;  // OnEnable 瞬间
bool flag2 = tweenLink.lastSeenActive && !activeInHierarchy; // OnDisable 瞬间

根据不同的 LinkBehaviour 策略:

  • PauseOnDisable:当物体隐藏时自动 Pause。

  • KillOnDisable:当物体隐藏或销毁时直接 Kill。

  • RestartOnEnable:当物体重新激活时自动 Restart。

  • 空指针防护 :当检测到 tweenLink.target == null(GameObject 已被 Destroy)时,会自动将 t.active = false 移出更新管线,彻底消除了手动管理动画生命周期的负担和空指针风险。

总结

TweenManager 展现了极高水平的性能优化考量:

  1. 零 GC 设计 :通过全类型强匹配的对象池 (_pooledTweeners / _PooledSequences) 和预分配数组,保障了运行期间的零内存分配。

  2. 高性能集合管理:通过延迟整理(Lazy Reorganization)避免了 O(N) 频发删除消耗。

  3. 两阶段更新安全机制:更新(Evaluation)与销毁(Despawn)解耦,彻底消除了动画遍历期间迭代器失效和集合修改冲突问题。

相关推荐
_ZHOURUI_H_21 小时前
Unity TCP底层极限拆解:粘包、半包、CRC、序列号、双缓冲到底怎么一起工作的?
网络·网络协议·tcp/ip·游戏·unity·游戏引擎·游戏开发
KindSuper_liu1 天前
Unity 游戏网络基础:从 TCP、UDP 到 HTTP、TLS 与 Best HTTP
网络·游戏·unity
qq_170264751 天前
unity里GameObject和Texture的内存归属
java·unity·游戏引擎
还是大剑师兰特2 天前
Unity3D(Unity)完整详细介绍
unity·游戏引擎
ellis19702 天前
u3d插件xLua[七]例6 Coroutine
unity
还是大剑师兰特2 天前
Unity 从零搭建简易3D场景(新手分步教程)
3d·unity·游戏引擎
神码编程2 天前
【Unity】TankBattle联机坦克大战(三)创建关卡(生成地块)
unity·游戏引擎·帧同步·坦克大战
淡海水2 天前
C#与常用数据结构源码剖析-全篇导览
开发语言·数据结构·unity·面试·职场和发展·c#
神码编程3 天前
【Unity】TankBattle联机坦克大战(四)坦克的核心功能(上)
unity·游戏引擎·帧同步·坦克大战