扩展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> All = new Dictionary>();

        // 通用数据
        [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);
        }

        /// 
        /// 只触发一次的接口
        /// 
        /// 
        /// 
        public abstract void OnInit(PlayableDirector playableDirector, FrameData info);

        /// 
        /// 每个动画帧触发
        /// 
        /// 
        /// 
        /// 
        public abstract void OnUpdate(PlayableDirector playableDirector, FrameData info, object playerData);

        /// 
        /// 自动添加生命周期回调
        /// 
        public abstract void OnExit();

        /// 
        /// 标记, 是否能在编辑器下使用
        /// 
        /// 
        public virtual bool CanPlayInEditorMode() { return true; }
        #endregion

        #region Help Funcs
        public void ResetInitState()
        {
            Inited = false;
        }

        /// 
        /// 重置Init状态
        /// 
        /// 
        public static void UnInit(PlayableDirector playableDirector)
        {
            var pool = All.TryGetNullableValue(playableDirector);
            if(pool != null)
            {
                foreach(var target in pool)
                {
                    target.ResetInitState();
                }
            }
        }

        /// 
        /// 强制移除,重置Init状态 -- 小心使用
        /// 
        /// 
        /// 
        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
    {
        #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> m_events
            = new Dictionary>();
#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(PlayableDirector director, T target, System.Action 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 在时间范围内只执行一次.

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

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

    [System.Serializable]
    public class SavableSelector where T : Component where V : struct
    {
        [SerializeField]
        public V Value;

        private System.Func _selector;

        public SavableSelector(System.Func 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 Scaler = new SerializableDatas.SavableSelector((_trans) =>
    {
        return _trans.lossyScale;
    });

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

var rawScale = Scaler.Value;

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