擴展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 做一些編輯器的顯示簡直太方便了. 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM