SpineManager 系统设计文档
最新文档更新 : 请看CSDN
https://blog.csdn.net/qq_39162566/article/details/148720013?spm=1001.2014.3001.5501
1. 概述
SpineManager
是用于管理 Spine 动画实例的核心单例类,主要负责 Spine 动画的对象池管理、分组轮转更新、LOD(细节层次)控制,确保性能与资源使用最优化。
SpineManagerExtend
作为其业务逻辑扩展,封装常用的实例生成和回收方法,避免主管理类与游戏业务逻辑耦合。
SpineManagerLODConfig
是通过 ScriptableObject 配置的参数文件,方便设计师在编辑器中调节 Spine 动画的 LOD 距离阈值、更新频率和分区数量。
2. SpineManager 核心功能
2.1 单例设计
- 采用真单例实现,避免静态构造顺序带来的隐患。
- 保证全局唯一 Spine 管理实例。
2.2 对象池管理
- 每个 SkeletonDataAsset 资源路径对应一个 Spine 实例对象池。
- 租赁实例时优先复用,减少 GC 和性能开销。
- 实例回收后自动隐藏并停用,挂载到管理隐藏根节点。
2.3 分组轮转更新机制
- 将所有激活 Spine 实例划分为 groupCount 个分区。
- 每帧只更新当前分区,均衡计算压力,避免性能峰值。
- 分区通过轮转索引轮流激活。
2.4 LOD 细节层次控制
- 依据主摄像机与实例的距离,自动调整动画更新帧间隔。
- 支持 3 个距离层次:高精度、中精度、低精度,对应不同更新频率。
- 距离阈值及更新帧间隔由 SpineManagerLODConfig 配置。
2.5 注册与注销机制
- 新租出实例时自动注册到最小负载的分区。
- 归还实例时从分区注销,放回代理对象池。
2.6 帧内更新流程
- 每帧调用 Update(),处理当前分区所有 Spine 代理。
- 按需更新动画状态和调用 LateUpdate。
- 维护脏标记优化 LateUpdate 调用。
3. SpineManagerExtend 扩展类说明
3.1 设计理念
- 将具体游戏业务逻辑与 Spine 管理解耦。
- 方便调用扩展方法生成和回收 Spine 动画实例。
- 代码整洁,职责清晰。
3.2 主要方法
- Spawn(spineManager, path, parent) 从 SpineManager 租赁实例,设置父节点和局部位置。
- DeSpawn(spineManager, skeletonAnimation) 清理动画状态、事件、Transform 变换,归还实例池。
4. SpineManagerLODConfig 配置文件说明
- highDetailDistance:高精度距离阈值,单位米,摄像机距离小于此值时使用高频更新。
- midDetailDistance:中精度距离阈值,摄像机距离介于高精度和中精度之间使用中频更新。
- highFrequency:高精度状态下动画更新的帧间隔,数值越小更新越频繁。
- midFrequency:中精度状态下的帧间隔。
- lowFrequency:低精度状态下的帧间隔,最大节省性能。
- groupCount:分区数量,将激活实例分散更新,减少单帧开销。
5. 典型使用示例
// 1. 游戏初始化时调用
SpineManager.Instance.Setup(mainCamera, lodConfig, LoadSpineAsset);
// 2. 生成动画实例
var spineAnim = SpineManager.Instance.Spawn("spine/hero_skeleton", parentTransform);
spineAnim.AnimationState.SetAnimation(0, "idle", true);
// 3. 游戏主循环调用
void Update()
{
SpineManager.Instance.Update();
}
// 4. 需要销毁时回收
SpineManager.Instance.DeSpawn(spineAnim);
6. 注意事项
- 不建议直接修改 SkeletonAnimation 内部的 __GroupId 和 __PoolIndex 字段。
- 回收时务必调用 DeSpawn 方法,保证状态清理和对象池管理正确。
- LOD 配置可根据实际性能需求调整,建议配合Profiler观察效果。
源码
资源加载委托定义, 你可以使用Resource也行,Addressable也行,或者自己写的AssetBundle管理工具也行,在初始化的时候处理
/// <summary>
/// 资源加载委托
/// </summary>
/// <typeparam name="T"> 返回的资源类型 </typeparam>
/// <param name="resPath"> assetBundle路径/Resource路径 </param>
/// <returns></returns>
public delegate T LoadResHandler<T>(string resPath);
SpineManagerLODConfig.cs
using UnityEngine;
namespace GameScripts.HeroTeam
{
[CreateAssetMenu(fileName = nameof(SpineManagerLODConfig), menuName = "SpineConfig/" + nameof(SpineManagerLODConfig))]
public class SpineManagerLODConfig : ScriptableObject
{
[Header("LOD 配置")]
[InspectorName("远距离范围触发的 LOD 显示距离")]
public float highDetailDistance = 10f;
[InspectorName("中等距离范围触发的 LOD 显示距离")]
public float midDetailDistance = 25f;
[Header("不同等级下更新的帧间隔(帧数间隔越大越省性能)")]
[InspectorName("近距离单位更新频率(帧间隔)")]
[Range(1, 16)]
public int highFrequency = 1;
[InspectorName("中距离单位更新频率(帧间隔)")]
[Range(1, 16)]
public int midFrequency = 2;
[InspectorName("远距离单位更新频率(帧间隔)")]
[Range(1, 16)]
public int lowFrequency = 4;
[Header("分区激活")]
[InspectorName("激活分区数量")]
[Tooltip("将激活的 Spine 分为 groupCount 个分区,每帧仅激活其中一部分以减小开销")]
[Range(1, 8)]
public int groupCount = 4;
}
}
SpineManager.cs
using System;
using System.Collections.Generic;
using Spine.Unity;
using UnityEngine;
namespace GameScripts.HeroTeam
{
/// <summary>
/// SpineManager
/// 负责 Spine 动画的统一管理,包括实例生成、更新调度、LOD 降级控制和对象池机制。
///
/// 主要职责:
/// 1. 对 SkeletonAnimation 实例进行对象池管理,避免频繁创建与销毁;
/// 2. 根据主摄像机与目标之间的距离,动态设定不同更新频率(LOD);
/// 3. 支持通过 ScriptableObject 配置参数,如更新间隔、分区数量、距离阈值;
/// 4. 提供 Spine 实例的租赁与归还接口;
/// 5. 使用分区轮转更新机制,避免同一帧中更新所有实例造成性能峰值;
/// 6. 采用真单例模式,确保唯一实例管理,避免资源冲突。
///
/// 使用方式:
/// - 调用 `SpineManager.Instance.Setup(...)` 进行初始化;
/// - 使用 `RentSkeletonAnimation()` 租用 Spine 实例;
/// - 使用 `RemandSkeletonAnimation()` 回收实例;
/// - 在主循环中调用 `Update()` 以驱动分区轮转和 LOD 调度;
///
/// 注意事项:
/// - 实例中的 `__GroupId` 和 `__PoolIndex` 为内部池管理字段,请勿手动更改;
/// - SpineManager 中的资源池默认支持自动清理,但不主动卸载资源;
/// </summary>
public class SpineManager
{
// 使用真单例方式,避免模板单例带来的静态构造顺序或泛型实例冲突问题
private SpineManager() { }
public static SpineManager Instance { private set; get; } = new SpineManager();
private Camera m_MainCamera;
private Transform m_trMainCamera;
/// <summary>
/// Spine动画代理,用于记录每个实例的运行状态、更新间隔与位置引用等
/// </summary>
public class SpineAgent
{
public int FrameCounter; // 帧计数器,用于计算是否需要更新
public int UpdateInterval; // 当前更新间隔(帧数)
public SkeletonAnimation Skeleton;
public Transform Transform;
public float PreUpdateTime; // 上次更新时间
public int LateUpdateable; // 是否需要LateUpdate(1表示需要)
}
// SpineAgent对象池,用于复用代理实例
private Stack<SpineAgent> m_stackSpineAgentPool = new Stack<SpineAgent>();
// SpineAgent分组,每组在一帧中被调度更新,避免性能峰值
private List<SpineAgent>[] m_AgentGroups;
private int m_iCurrentGroupIndex = 0;
// LOD 相关参数
private float m_fHighDetailDistance = 10f;
private float m_fMidDetailDistance = 25f;
private int m_iHighFrequency = 1; // 高精度,帧间隔为1
private int m_iMidFrequency = 2; // 中精度
private int m_iLowFrequency = 4; // 低精度
private bool m_bLateUpdateDirty = false; // LateUpdate脏标记
// Spine 实例缓存的父节点(隐藏在场景中)
private Transform m_trSpineCacheRoot;
// 每种资源路径对应一个对象池
private Dictionary<string, UnityEngine.Pool.ObjectPool<SkeletonAnimation>> m_dicSpineObjPool = new();
private Dictionary<string, SkeletonDataAsset> m_dicShareSkeletonDataAssets = new();
private const string m_szPoolSpineTag = "[Pool Spine]";
// 外部资源加载委托(支持自定义加载逻辑,如AB或Addressables)
private LoadResHandler<SkeletonDataAsset> loadResHandler;
/// <summary>
/// 初始化 SpineManager,必须调用一次
/// </summary>
/// <param name="mainCamera">用于LOD计算的主摄像机</param>
/// <param name="setting">LOD与分区设置</param>
/// <param name="loadResHandler">资源加载回调</param>
public void Setup(Camera mainCamera, SpineManagerLODConfig setting, LoadResHandler<SkeletonDataAsset> loadResHandler)
{
m_MainCamera = mainCamera;
m_trMainCamera = mainCamera.transform;
m_fHighDetailDistance = setting.highDetailDistance;
m_fMidDetailDistance = setting.midDetailDistance;
m_iHighFrequency = setting.highFrequency;
m_iMidFrequency = setting.midFrequency;
m_iLowFrequency = setting.lowFrequency;
m_AgentGroups = new List<SpineAgent>[setting.groupCount];
for (int i = 0; i < setting.groupCount; i++)
{
m_AgentGroups[i] = new();
}
m_trSpineCacheRoot = new GameObject("[SpineManager]").transform;
GameObject.DontDestroyOnLoad(m_trSpineCacheRoot.gameObject);
m_trSpineCacheRoot.gameObject.SetActive(false);
this.loadResHandler = loadResHandler;
Debug.Assert(loadResHandler != null, "SpineManager.Setup 初始化必须配置资源获取句柄");
}
/// <summary>
/// 租赁一个SkeletonAnimation实例(会自动从池中复用或创建)
/// </summary>
public SkeletonAnimation RentSkeletonAnimation(string szSkeletonDataAssetPath)
{
if (!m_dicSpineObjPool.TryGetValue(szSkeletonDataAssetPath, out var pool))
{
if (!m_dicShareSkeletonDataAssets.TryGetValue(szSkeletonDataAssetPath, out var dataAsset))
{
dataAsset = loadResHandler(szSkeletonDataAssetPath);
Debug.Assert(dataAsset != null, $"LoadResSync Fail, path: {szSkeletonDataAssetPath}");
m_dicShareSkeletonDataAssets.Add(szSkeletonDataAssetPath, dataAsset);
dataAsset.__InstancePoolKey = szSkeletonDataAssetPath;
}
pool = new UnityEngine.Pool.ObjectPool<SkeletonAnimation>(() =>
{
var obj = new GameObject(m_szPoolSpineTag);
obj.transform.SetParent(m_trSpineCacheRoot, false);
return SkeletonAnimation.AddToGameObject(obj, dataAsset);
}, on_get_obj =>
{
Register(on_get_obj);
}, on_release_obj =>
{
UnRegister(on_release_obj);
on_release_obj.transform.SetParent(m_trSpineCacheRoot);
}, on_destory_obj =>
{
GameObject.Destroy(on_destory_obj.gameObject);
}, true, 0);
m_dicSpineObjPool[szSkeletonDataAssetPath] = pool;
}
return pool.Get();
}
/// <summary>
/// 将SkeletonAnimation归还回对象池
/// </summary>
public void RemandSkeletonAnimation(SkeletonAnimation skeleton)
{
if (skeleton == null || skeleton.skeletonDataAsset == null)
{
#if UNITY_EDITOR
Debug.LogError("尝试归还无效的骨骼");
#endif
return;
}
if (m_dicSpineObjPool.TryGetValue(skeleton.skeletonDataAsset.__InstancePoolKey, out var pool))
{
pool.Release(skeleton);
}
}
/// <summary>
/// 注册新租出的SkeletonAnimation到轮转分组中
/// </summary>
private void Register(SkeletonAnimation skeleton)
{
if (skeleton == null) return;
skeleton.enabled = false;
// 找到最少的分组,平衡负载
int group = 0;
int minCount = int.MaxValue;
for (int i = 0; i < m_AgentGroups.Length; i++)
{
int tmpCount = m_AgentGroups[i].Count;
if (tmpCount < minCount)
{
minCount = tmpCount;
group = i;
}
}
SpineAgent agent = m_stackSpineAgentPool.Count == 0 ? new SpineAgent() : m_stackSpineAgentPool.Pop();
agent.Skeleton = skeleton;
agent.Transform = skeleton.transform;
agent.FrameCounter = group;
agent.UpdateInterval = m_iHighFrequency;
agent.PreUpdateTime = TimeUtils.CurrentTime;
agent.LateUpdateable = 0;
m_AgentGroups[group].Add(agent);
skeleton.__GroupId = group;
skeleton.__PoolIndex = m_AgentGroups[group].Count - 1;
}
/// <summary>
/// 从更新分组中移除SkeletonAnimation,回收到代理池中
/// </summary>
private void UnRegister(SkeletonAnimation skeleton)
{
if (skeleton == null) return;
#if UNITY_EDITOR
Debug.Assert(skeleton.__GroupId != -1, "请不要手动修改 SkeletonAnimation内部的__GroupId");
Debug.Assert(skeleton.__PoolIndex != -1, "请不要手动修改 SkeletonAnimation内部的__PoolIndex");
#endif
var pool = m_AgentGroups[skeleton.__GroupId];
if (skeleton.__PoolIndex >= 0 && skeleton.__PoolIndex < pool.Count && pool[skeleton.__PoolIndex].Skeleton == skeleton)
{
SleepSkeleton(skeleton.__PoolIndex, pool);
}
else
{
#if UNITY_EDITOR
Debug.LogWarning($"请不要手动修改 SkeletonAnimation内部的__PoolIndex");
#endif
for (int i = 0; i < pool.Count; i++)
{
if (pool[i].Skeleton == skeleton)
{
SleepSkeleton(i, pool);
return;
}
}
#if UNITY_EDITOR
Debug.LogError("未能从分组中正确回收Skeleton,可能__PoolIndex索引错误或外部干预!");
#endif
}
}
/// <summary>
/// 执行实际回收,将SkeletonAnimation从激活组中移除
/// </summary>
private void SleepSkeleton(int poolIndex, List<SpineAgent> pool)
{
var release_agent = pool[poolIndex];
m_stackSpineAgentPool.Push(release_agent);
var swap_agent = pool[pool.Count - 1];
swap_agent.Skeleton.__PoolIndex = poolIndex;
pool[poolIndex] = swap_agent;
pool.RemoveAt(pool.Count - 1);
}
/// <summary>
/// 在每帧中调用,驱动轮转更新与LOD分发
/// </summary>
public void Update()
{
if (m_MainCamera == null) return;
List<SpineAgent> group = m_AgentGroups[m_iCurrentGroupIndex];
Vector3 camPos = m_trMainCamera.position;
m_bLateUpdateDirty = false;
foreach (var agent in group)
{
float dist = Vector3.Distance(camPos, agent.Transform.position);
if (dist < m_fHighDetailDistance)
agent.UpdateInterval = m_iHighFrequency;
else if (dist < m_fMidDetailDistance)
agent.UpdateInterval = m_iMidFrequency;
else
agent.UpdateInterval = m_iLowFrequency;
agent.FrameCounter++;
if (agent.FrameCounter >= agent.UpdateInterval)
{
agent.FrameCounter = 0;
float deltaTime = TimeUtils.CurrentTime - agent.PreUpdateTime;
agent.PreUpdateTime = TimeUtils.CurrentTime;
agent.Skeleton.UpdateSkeleton(deltaTime);
agent.LateUpdateable = 1;
m_bLateUpdateDirty = true;
}
}
if (m_bLateUpdateDirty)
{
foreach (var agent in group)
{
if (agent.LateUpdateable == 1)
{
agent.LateUpdateable = 0;
agent.Skeleton.LateUpdate();
}
}
}
// 分组轮转更新
m_iCurrentGroupIndex = (m_iCurrentGroupIndex + 1) % m_AgentGroups.Length;
}
}
}
SpineManagerExtend.cs
using Spine.Unity;
using UnityEngine;
namespace GameScripts.HeroTeam
{
/// <summary>
/// SpineManager的业务逻辑扩展类,用于封装与具体游戏业务相关的Spine操作。
///
/// 设计理念:
/// - SpineManager 专注于Spine动画的性能优化、实例创建与回收管理,不直接参与具体游戏逻辑。
/// - 各个游戏项目对Spine动画的需求可能不同,业务逻辑应由扩展类负责实现,避免SpineManager臃肿。
/// - 虽然有些团队喜欢创建大量Utils类(如XXXGameSpineUtils.cs),
/// 但过多分散的工具类会增加项目复杂度和维护成本。
/// - 采用注入式扩展(Extension Method)方式,将业务逻辑附加于SpineManager,方便管理和调用。
///
/// 主要功能:
/// - Spawn:从对象池租用SkeletonAnimation实例,并设置父节点和初始位置。
/// - DeSpawn:清理SkeletonAnimation动画状态和Transform属性,清除事件监听,归还对象池。
///
/// 备注:
/// - 业务扩展方法应保持简洁,避免修改SpineManager核心逻辑。
/// - 该设计模式提升代码整洁性和维护性,方便后续根据不同游戏需求定制扩展。
/// </summary>
public static class SpineManagerExtend
{
/// <summary>
/// 从对象池租用SkeletonAnimation实例,并将其挂载到指定父节点下,位置重置为零。
/// </summary>
/// <param name="spineManager">SpineManager实例</param>
/// <param name="szSkeletonDataAssetPath">骨骼数据资源路径</param>
/// <param name="parent">父Transform</param>
/// <returns>租用的SkeletonAnimation实例</returns>
public static SkeletonAnimation Spawn(this SpineManager spineManager, string szSkeletonDataAssetPath, Transform parent)
{
var skeleton = spineManager.RentSkeletonAnimation(szSkeletonDataAssetPath);
skeleton.transform.SetParent(parent);
skeleton.transform.localPosition = Vector3.zero;
return skeleton;
}
/// <summary>
/// 清理SkeletonAnimation的动画状态、Transform和事件监听,归还至对象池。
/// </summary>
/// <param name="spineManager">SpineManager实例</param>
/// <param name="skeletonAnimation">待回收的SkeletonAnimation实例</param>
public static void DeSpawn(this SpineManager spineManager, SkeletonAnimation skeletonAnimation)
{
// 清理动画状态,恢复默认姿势
skeletonAnimation.AnimationState.ClearTracks();
skeletonAnimation.AnimationState.TimeScale = 1f;
skeletonAnimation.Skeleton?.SetToSetupPose();
skeletonAnimation.Skeleton?.SetSlotsToSetupPose();
skeletonAnimation.Skeleton?.UpdateWorldTransform();
// 重置Transform,确保对象状态一致
skeletonAnimation.transform.localPosition = Vector3.zero;
skeletonAnimation.transform.localRotation = Quaternion.identity;
skeletonAnimation.transform.localScale = Vector3.one;
// 清除所有动画事件监听,避免遗留回调
skeletonAnimation.AnimationState.ClearAllAnimationEventHandlers();
skeletonAnimation.AnimationState.ClearListenerNotifications();
// 归还对象池,会挂载到隐藏节点并停止自动更新
spineManager.RemandSkeletonAnimation(skeletonAnimation);
}
}
}
SkeletonAnimation中的改动
请加入到你项目中的Spine插件 任意版本,SkeletonAnimation.cs文件中
#region SpineManager Field
/// <summary>
/// 不要手动修改
/// </summary>
public int __GroupId = -1;
/// <summary>
/// 不要手动修改
/// </summary>
public int __PoolIndex = -1;
#endregion
SkeletonDataAsset.cs
增加一个 key 用来定位,避免池中遍历查找
#region SpineManager Filed
public string __InstancePoolKey = null;
#endregion
AnimationState.cs
AnimationState中的改动: 将 Start,End,等, 还有自定义的Event改造记录他们的监听和移除的事件, 然后封装一个方法 ClearAllAnimationEventHandlers 在回收的时候移除,如果有的开发者粗心大意,只做了事件监听,没有移除, 比如 .Start += OnStart, 但是忘记在回收的时候 .Start-=OnStart, 那么SpineManager会兜底帮你回收, 在编辑器模式下 会打印告诉你哪些方法没有回收
#region SpineManager Fields
private TrackEntryDelegate m_Start;
private TrackEntryDelegate m_Interrupt;
private TrackEntryDelegate m_End;
private TrackEntryDelegate m_Dispose;
private TrackEntryDelegate m_Complete;
// 用一个链表统一管理所有事件回调,方便统一清理
private readonly LinkedList<TrackEntryDelegate> m_SystemEventLinkedList = new LinkedList<TrackEntryDelegate>();
private readonly LinkedList<TrackEntryEventDelegate> m_CustomEventLinkedList = new LinkedList<TrackEntryEventDelegate>();
public event TrackEntryDelegate Start
{
add
{
m_SystemEventLinkedList.AddLast(value);
m_Start += value;
}
remove
{
m_SystemEventLinkedList.Remove(value);
m_Start -= value;
}
}
public event TrackEntryDelegate Interrupt
{
add
{
m_SystemEventLinkedList.AddLast(value);
m_Interrupt += value;
}
remove
{
m_SystemEventLinkedList.Remove(value);
m_Interrupt -= value;
}
}
public event TrackEntryDelegate End
{
add
{
m_SystemEventLinkedList.AddLast(value);
m_End += value;
}
remove
{
m_SystemEventLinkedList.Remove(value);
m_End -= value;
}
}
public event TrackEntryDelegate Dispose
{
add
{
m_SystemEventLinkedList.AddLast(value);
m_Dispose += value;
}
remove
{
m_SystemEventLinkedList.Remove(value);
m_Dispose -= value;
}
}
public event TrackEntryDelegate Complete
{
add
{
m_SystemEventLinkedList.AddLast(value);
m_Complete += value;
}
remove
{
m_SystemEventLinkedList.Remove(value);
m_Complete -= value;
}
}
public delegate void TrackEntryEventDelegate(TrackEntry trackEntry, Event e);
private TrackEntryEventDelegate m_Event;
public event TrackEntryEventDelegate Event
{
add
{
m_Event += value;
m_CustomEventLinkedList.AddLast(value);
}
remove
{
m_Event -= value;
m_CustomEventLinkedList.Remove(value);
}
}
/// <summary>
/// 统一清理所有 Spine 动画事件,避免事件泄漏和重复调用。
/// </summary>
public void ClearAllAnimationEventHandlers()
{
#if UNITY_EDITOR
if (m_SystemEventLinkedList.Count > 0 || m_CustomEventLinkedList.Count > 0)
{
UnityEngine.Debug.LogWarning(
$"[SpineManager Warning] 存在未手动清理的事件处理器,可能导致内存泄漏或重复回调。" +
$"建议在生命周期末尾(如 OnDisable/OnDestroy/OnRecycle)手动调用 -= 清理事件。");
}
#endif
foreach (var handler in m_SystemEventLinkedList)
{
#if UNITY_EDITOR
UnityEngine.Debug.Log($"[AnimationState.SystemEvent] 清理系统事件: {handler.Method.DeclaringType}.{handler.Method.Name}");
#endif
m_Start -= handler;
m_Interrupt -= handler;
m_End -= handler;
m_Dispose -= handler;
m_Complete -= handler;
}
foreach (var handler in m_CustomEventLinkedList)
{
#if UNITY_EDITOR
UnityEngine.Debug.Log($"[AnimationState.CustomEvent] 清理自定义事件: {handler.Method.DeclaringType}.{handler.Method.Name}");
#endif
Event -= handler;
}
m_SystemEventLinkedList.Clear();
m_CustomEventLinkedList.Clear();
}
public void AssignEventSubscribersFrom(AnimationState src)
{
//先清除之前的事件
ClearAllAnimationEventHandlers();
//复制事件
m_Event = src.m_Event;
m_Start = src.m_Start;
m_Interrupt = src.m_Interrupt;
m_End = src.m_End;
m_Dispose = src.m_Dispose;
m_Complete = src.m_Complete;
//记录句柄
foreach (var handler in src.m_SystemEventLinkedList)
{
if (!m_SystemEventLinkedList.Contains(handler))
m_SystemEventLinkedList.AddLast(handler);
}
foreach (var handler in src.m_CustomEventLinkedList)
{
if (!m_CustomEventLinkedList.Contains(handler))
m_CustomEventLinkedList.AddLast(handler);
}
}
//禁用,自己用额外的方式处理,避免风险
// public void AddEventSubscribersFrom(AnimationState src)
// {
// ClearAllAnimationEventHandlers();
// Event += src.m_Event;
// m_Start += src.m_Start;
// m_Interrupt += src.m_Interrupt;
// m_End += src.m_End;
// m_Dispose += src.m_Dispose;
// m_Complete += src.m_Complete;
// }
#endregion