• 扩展Unity的Timeline系统


      最近有些地方要用到 Timeline 这样的系统, 因为 Unity 自己提供了一套, 就直接拿来用了, 发现这套 Timeline 设计的比较复杂, 而且很多点都缺失, 甚至生命周期都不完善, 有点为了解耦而强行 MVC / MVVM 的设计思路, 扩展起来还是很麻烦的.

      简单来说要做扩展只要生成两份代码就行了, 一个是继承 PlayableAsset, ITimelineClipAsset 的 Clip, 可以把它看成是创建 Timeline 行为的入口, 它既是入口, 又是编辑器工具下的可视化对象, 另一个是继承 PlayableBehaviour 的, 它就是实际的 Timeline 逻辑对象, 包含了一些运行生命周期.

      首先问题就是生命周期的问题, 它是一个积分类型的系统, 并且缺失了 OnComplete 的结束回调.

      看看 PlayableBehaviour 的主要生命周期重载:

        public override void OnBehaviourPlay(Playable playable, FrameData info)
        {
            // 开始播放时触发
        }
        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            // 播放后每帧触发
        }

      如下图 OnBehaviourPlay 在系统运行到这个 Clip 的起点的时候, 会触发, 而如果系统运行到这个 Clip 还没结束的时候, 进行了暂停, 然后再次恢复播放的话, 它还是会触发 : 

     

      举例来说一个UI按钮的点击, 在进入状态时触发点击, 退出状态时再次点击, 那么在这里首先是无法实现, 然后是点击的次数无法控制, 因为每次暂停都可能造成错误触发点击.

      那么就需要扩展一套生命周期的控制, 来完成补充生命周期以及触发控制逻辑了. 直接通过继承 PlayableBehaviour 来创建一个基类扩展, 其它只要继承就行了 : 

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Playables;
    using UnityEngine.Timeline;
    
    namespace UnityTimelineExtention
    {
        [System.Serializable]
        public abstract class PlayableBaseBehaviour : PlayableBehaviour
        {
            // 全局控制
            private static readonly Dictionary<PlayableDirector, HashSet<PlayableBaseBehaviour>> All = new Dictionary<PlayableDirector, HashSet<PlayableBaseBehaviour>>();
    
            // 通用数据
            [SerializeField]
            [Header("开始时间")]
            public double startTime = 0.0;
            [SerializeField]
            [Header("开始时间")]
            public double endTime = 0.0;
    
            // 缓存数据
            protected PlayableDirector playableDirector { get; private set; }
    
            // 属性
            public double duration { get { return endTime - startTime; } }
            protected bool Inited { get; set; }
    
            #region Main Funcs
            public override void OnBehaviourPlay(Playable playable, FrameData info)
            {
                base.OnBehaviourPlay(playable, info);
                if(false == Inited)
                {
                    playableDirector = playable.GetGraph().GetResolver() as PlayableDirector;
                    if(Application.isPlaying || CanPlayInEditorMode())
                    {
                        All.GetValue(playableDirector).Add(this);
                        // 结束回调, 写在功能前
                        UnityTimelineEventDispatcher.Instance.SetEndCall(playableDirector, this, (_tag) =>
                        {
                            OnExit();
                        });
                        OnInit(playableDirector, info);
                    }
                }
                Inited = true;
            }
            public override void ProcessFrame(Playable playable, FrameData info, object playerData)
            {
                base.ProcessFrame(playable, info, playerData);
                OnUpdate(playableDirector, info, playerData);
            }
    
            /// <summary>
            /// 只触发一次的接口
            /// </summary>
            /// <param name="playable"></param>
            /// <param name="info"></param>
            public abstract void OnInit(PlayableDirector playableDirector, FrameData info);
    
            /// <summary>
            /// 每个动画帧触发
            /// </summary>
            /// <param name="playableDirector"></param>
            /// <param name="info"></param>
            /// <param name="playerData"></param>
            public abstract void OnUpdate(PlayableDirector playableDirector, FrameData info, object playerData);
    
            /// <summary>
            /// 自动添加生命周期回调
            /// </summary>
            public abstract void OnExit();
    
            /// <summary>
            /// 标记, 是否能在编辑器下使用
            /// </summary>
            /// <returns></returns>
            public virtual bool CanPlayInEditorMode() { return true; }
            #endregion
    
            #region Help Funcs
            public void ResetInitState()
            {
                Inited = false;
            }
    
            /// <summary>
            /// 重置Init状态
            /// </summary>
            /// <param name="playableDirector"></param>
            public static void UnInit(PlayableDirector playableDirector)
            {
                var pool = All.TryGetNullableValue(playableDirector);
                if(pool != null)
                {
                    foreach(var target in pool)
                    {
                        target.ResetInitState();
                    }
                }
            }
    
            /// <summary>
            /// 强制移除,重置Init状态 -- 小心使用
            /// </summary>
            /// <param name="playableDirector"></param>
            /// <param name="target"></param>
            public static void RemoveFromPool(PlayableDirector playableDirector, PlayableBaseBehaviour target)
            {
                var pool = All.TryGetNullableValue(playableDirector);
                if(pool != null)
                {
                    if(pool.Remove(target))
                    {
                        target.ResetInitState();
                    }
                }
            }
            #endregion
        }
    }

      这样就添加了生命周期, 重命名成了 OnInit, OnUpdate, OnExit 这样的三个, 只有 OnExit 是额外添加的, 需要通过另外的监视器 UnityTimelineEventDispatcher 来实现 : 

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Playables;
    
    namespace UnityTimelineExtention
    {
        using Common;
    
        public class UnityTimelineEventDispatcher : Singleton<UnityTimelineEventDispatcher>
        {
            #region Defines
            public class PlayableEvent
            {
                public PlayableDirector playableDirector;
                public Common.ObsoluteTime timer;
                public double startTime;
                public double endTime;
                public double duration { get { return endTime - startTime; } }
                public System.Action endCall;
    
                public void Invoke()
                {
                    if(endCall != null)
                    {
                        var temp = endCall;
                        endCall = null;
                        temp.Invoke();
                    }
                }
            }
            #endregion
    
            private bool _reigsted = false;
    
            private readonly Dictionary<PlayableDirector, MyDictionary<PlayableBaseBehaviour, PlayableEvent>> m_events
                = new Dictionary<PlayableDirector, MyDictionary<PlayableBaseBehaviour, PlayableEvent>>();
    #region Overrides
            protected override void Initialize()
            {
                RegisterUpdate();
            }
    
            protected override void UnInitialize()
            {
                if(Application.isPlaying)
                {
                    Core.CoroutineRoot.instance.update -= Tick;
                }
    #if UNITY_EDITOR
                UnityEditor.EditorApplication.update -= Tick;
    #endif
                _reigsted = false;
            }
            #endregion
    
            #region Main Funcs
            public void SetEndCall<T>(PlayableDirector director, T target, System.Action<T> endCall) where T : PlayableBaseBehaviour
            {
                if(director)
                {
                    if(target != null)
                    {
                        if(m_events.ContainsKey(director) == false)
                        {
                            director.stopped -= OnStop;
                            director.stopped += OnStop;
                        }
                        PopEnd(director, director.time);
                        var playableEvent = m_events.GetValue(director).GetValue(target);
                        playableEvent.playableDirector = director;
                        playableEvent.startTime = target.startTime;
                        playableEvent.endTime = target.endTime;
                        playableEvent.timer = new Common.ObsoluteTime();
                        playableEvent.endCall = () =>
                        {
                            endCall.Invoke(target);
                        };
                    }
                }
            }
    
            private void Tick()
            {
                foreach(var kv in m_events)
                {
                    kv.Value.Remove((_tag, _playableEvent) =>
                    {
                        if(_playableEvent.playableDirector)
                        {
                            var gap = _playableEvent.playableDirector.time - (_playableEvent.startTime + _playableEvent.duration - 0.001);
                            if(gap >= 0.0)
                            {
                                _playableEvent.Invoke();
                                return true;
                            }
                        }
                        else
                        {
                            if(_playableEvent.timer.ElapsedSeconds() >= _playableEvent.duration)
                            {
                                _playableEvent.Invoke();
                                return true;
                            }
                        }
    
                        return false;
                    });
                }
            }
            #endregion
    
            #region Help Funcs
            private void PopEnd(PlayableDirector director, double time)
            {
                var events = m_events.TryGetNullableValue(director);
                if(events != null)
                {
                    events.Remove((_tag, _playableEvent) =>
                    {
                        if(time >= (_playableEvent.startTime + _playableEvent.duration))
                        {
                            PlayableBaseBehaviour.RemoveFromPool(director, _tag);
                            _playableEvent.Invoke();
                            return true;
                        }
                        return false;
                    });
                }
            }
            public void OnStop(PlayableDirector playableDirector)
            {
                if(playableDirector)
                {
                    Debug.Log(playableDirector.gameObject.name + " Stopped");
                    var events = m_events.TryGetNullableValue(playableDirector);
                    if(events != null)
                    {
                        PopEnd(playableDirector, playableDirector.duration + UnityTimelineTools.Epsilon);
                        events.Clear();
                    }
                }
            }
            private void RegisterUpdate()
            {
                if(false == _reigsted)
                {
                    _reigsted = true;
                    if(Application.isPlaying)
                    {
                        Core.CoroutineRoot.instance.update -= Tick;
                        Core.CoroutineRoot.instance.update += Tick;
                    }
                    else
                    {
    #if UNITY_EDITOR
                        UnityEditor.EditorApplication.update -= Tick;
                        UnityEditor.EditorApplication.update += Tick;
    #endif
                    }
                }
            }
            #endregion
        }
    }

      这里主要就是通过监视 PlayableDirector 的时间来测试一个 PlayableBehaviour 是否已经到达结束, 触发它的 OnExit 方法.

      这里需要一提的是在运行时的 PlayableBehaviour 并没有自己的开始结束时间信息, 需要从 TimelineClip 里面去获取, 而 TimelineClip 又是从 PlayableAsset 的 TrackAsset 中找, 需要绕一个大圈, 非常麻烦, 而我们在重载接口里得到的输入一般是 Playable 或者 PlayableGraph, 要获取 PlayableBehaviour 也是需要显式转换 : 

    playableDirector = playable.GetGraph().GetResolver() as PlayableDirector;

      找资料都要找半天... 

      通过这样的方式来找对应 TimelineClip : 

        // find clip
        public static TimelineClip FindClip(PlayableDirector director, PlayableAsset asset)
        {
            if(director)
            {
                var trackAssets = ((TimelineAsset)director.playableAsset).GetOutputTracks();
                foreach(var trackers in trackAssets)
                {
                    foreach(var clip in trackers.GetClips())
                    {
                        if(clip.asset == asset)
                        {
                            return clip;
                        }
                    }
                }
            }
            return null;
        }
        // find clip
        public static TimelineClip FindClip(PlayableGraph graphy, PlayableAsset asset)
        {
            var director = graphy.GetResolver() as PlayableDirector;
            return FindClip(director, asset);
        }
        // find clip
        public static TimelineClip FindClip(Playable playable, PlayableAsset asset)
        {
            return FindClip(playable.GetGraph(), asset);
        }

      有了这些, 我们现在需要的就是创建代码了, 做个工具生成代码就行了:

      两个代码 template : 

      1. Clip

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Playables;
    using UnityEngine.Timeline;
    
    namespace UnityTimelineExtention
    {
        public class #CLASSNAME#Clip : PlayableAsset, ITimelineClipAsset
        {
            // 用来同步数据的
            public #CLASSNAME#Behaviour template;
    
            public ClipCaps clipCaps { get { return ClipCaps.None; } }
            // 重写初始化长度
            //public override double duration
            //{
            //    get { return 0.5f; }
            //}
    
            public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
            {
                var playable = ScriptPlayable<#CLASSNAME#Behaviour>.Create(graph, template);
                var playable#CLASSNAME#Behaviour = playable.GetBehaviour();
                var clip = UnityTimelineTools.FindClip(graph, this);
                if(clip != null)
                {
                    playable#CLASSNAME#Behaviour.startTime = clip.start;
                     playable#CLASSNAME#Behaviour.endTime = clip.end;
                }
                return playable;
            }
        }
    }

      2. Behaviour

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Playables;
    using UnityEngine.Timeline;
    
    namespace UnityTimelineExtention
    {
        [System.Serializable]
        public class #CLASSNAME#Behaviour : PlayableBaseBehaviour
        {
            // 基类数据
            /* playableDirector */
    
            // 当前动作开始回调
            public override void OnInit(PlayableDirector playableDirector, FrameData info)
            {
                // 功能
                Debug.Log("#CLASSNAME#Behaviour Play");
            }
    
            public override void OnUpdate(PlayableDirector playableDirector, FrameData info, object playerData)
            {
                // update
            }
    
            public override void OnExit()
            {
                // end
                Debug.Log("#CLASSNAME#Behaviour End");
            }
        }
    }

      这样生成的代码里面, XXXBehaviour 就有了生命周期了, 并且有了开始结束时间, 并且能限制 OnInit 在时间范围内只执行一次.

       这里有个特殊的对象, TimelineAsset 也就是在一个 TimelineAsset 里面嵌套另外一个, 编辑器本身没有支持, 所以它并不是一个完美嵌套的设计, 我这里就直接按照普通对象来生成代码然后添加强行播放逻辑:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Playables;
    using UnityEngine.Timeline;
    
    namespace UnityTimelineExtention
    {
        [System.Serializable]
        public class TimelineAssetPlayerBehaviour : PlayableBaseBehaviour
        {
            // 面板序列化数据
            [SerializeField]
            public TimelineAsset timelineAsset;
    
            // 缓存数据
            private PlayableDirector assetDirector;
            private bool isPlaying = false;
    
            public override void OnInit(PlayableDirector playableDirector, FrameData info)
            {
                // 功能
                Debug.Log("TimelineAssetPlayerBehaviour Play");
                if(timelineAsset)
                {
                    if(assetDirector == false)
                    {
                        assetDirector = UnityTimelineTools.PlayTimeline(timelineAsset);
                    }
                    assetDirector.Pause();
                    assetDirector.time = 0;
                    assetDirector.Evaluate();
                    isPlaying = true;
                }
            }
    
            public override void OnUpdate(PlayableDirector playableDirector, FrameData info, object playerData)
            {
                Tick((float)(playableDirector.time - this.startTime));
            }
    
            public override void OnExit()
            {
                isPlaying = false;
                if(assetDirector)
                {
                    assetDirector.time = assetDirector.duration + UnityTimelineTools.Epsilon;
                    assetDirector.Evaluate();
                    assetDirector.Stop();
                }
            }
    
            private void Tick(float elapsedTime)
            {
                if(isPlaying && assetDirector)
                {
                    assetDirector.time = elapsedTime;
                    assetDirector.Evaluate();
                }
            }
        }
    }

      在播放过程中使用 Evaluate 的方式来进行, 保证逻辑一致, 并且在结束时调用 Stop 能正确触发结束回调.

      所以可见, 一个 Behaviour 的开始结束时间信息非常重要.

    ------------------- 一些小技巧 -------------------

      Unity系统的序列化支持泛型对象了, 比如下面的我需要拖入一个 Transform 来序列化这个 Transform 的绝对缩放可以这样写 : 

        [System.Serializable]
        public class SavableSelector<T, V> where T : Component where V : struct
        {
            [SerializeField]
            public V Value;
    
            private System.Func<T, V> _selector;
    
            public SavableSelector(System.Func<T, V> selector)
            {
                _selector = selector;
            }
    
            [Sirenix.OdinInspector.ShowInInspector]
            [Sirenix.OdinInspector.LabelText("拖入对象 --> ")]
            public T Target
            {
                get { return null; }
                set
                {
                    if(value)
                    {
                        this.Value = _selector.Invoke(value);
                    }
                }
            }
        }

      我在 XXXBehaviour 中这样来序列化 : 

        [SerializeField]
        [Header("记录原始缩放")]
        public SerializableDatas.SavableSelector<Transform, Vector3> Scaler = new SerializableDatas.SavableSelector<Transform, Vector3>((_trans) =>
        {
            return _trans.lossyScale;
        });

      面板上可以得到这样的显示, 序列化数据就直接获取即可:

    var rawScale = Scaler.Value;

      依赖 OdinInspector 做一些编辑器的显示简直太方便了. 

  • 相关阅读:
    IDEA设置类级注释和方法级注释
    简单的后台管理系统demo(基于Spring Boot)
    MyBatis中if test传入0值时不识别
    MySQL插入时间数据与系统时间差8小时
    MySQL按字符串中部分数值排序
    二叉查找树
    40个提升网站用户体验的jQuery插件推荐 40个提升网站用户体验的jQuery插件推荐
    纯CSS3实现的8种Loading动画效果
    Web前端框架汇总
    最大公约数 最小公倍数
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/15870573.html
Copyright © 2020-2023  润新知