Unity SpineManager 系统设计文档


SpineManager 系统设计文档



最新文档更新 : 请看CSDN

https://blog.csdn.net/qq_39162566/article/details/148720013?spm=1001.2014.3001.5501

image.png

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

image.png



SkeletonDataAsset.cs

增加一个 key 用来定位,避免池中遍历查找


#region SpineManager Filed

        public string __InstancePoolKey = null;

        #endregion

image.png


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




-

Unity UI 中最干净的点击区域实现:RaycastZone 完整实战讲解

评 论