Unity PlayableDirector(Timeline) forward and backward processing

2023.5.30 update

 Made a modification for the bug proposed by @欢乐的小胖子小伴.

Just replace  TimelineDirector.cs  directly :

using System;
using DG.Tweening;
using UnityEngine;
using UnityEngine.Playables;



[RequireComponent(typeof(PlayableDirector))]
public class TimelineDirector : MonoBehaviour
{
    #region ENUM
    public enum Status
    {
        NULL,
        PLAYING,
        PAUSED,
        STOPPED,
    }

    public enum Direction
    {
        NULL,
        FORWARD,
        BACKWARD
    }

    #endregion

    [SerializeField]
    private PlayableDirector m_playableDirector;

    [Range(0f, 1f)]
    public float PlaySpeed = 1f;

    /// <summary>
    /// 播放模式
    /// </summary>
    public WrapMode WrapMode = WrapMode.Once;

    /// <summary>
    /// 开始播放事件, 返回时 时间点,和触发时方向
    /// </summary>
    public Action<double, Direction> OnPlay;

    /// <summary>
    /// 暂停播放事件, 返回时 时间点,和触发时方向
    /// </summary>
    public Action<double, Direction> OnPause;

    /// <summary>
    /// 停止播放事件, 返回时 时间点,和触发时方向
    /// </summary>
    public Action<double, Direction> OnStop;

    /// <summary>
    /// 继续播放事件, 返回时 时间点,和触发时方向
    /// </summary>
    public Action<double, Direction> OnContinue;

    /// <summary>
    /// Timeline长度
    /// </summary>
    public double Duration { get; private set; } = -1f;

    /// <summary>
    /// 当前播放状态(如果用不到可是删除,现在这个字段只是一个状态的记录)
    /// </summary>
    public Status CurrentPlayStatus { get; private set; } = Status.NULL;

    /// <summary>
    /// 当前播放方向(如果用不到可是删除,现在这个字段只是一个状态的记录)
    /// </summary>
    public Direction CurrentPlayDirection { get; private set; } = Direction.NULL;

    /// <summary>
    /// 当前播放进度
    /// </summary>
    public double CurrentTime { get; private set; } = 0d;

    private Tweener m_timeTween;

    private void Awake()
    {
        m_playableDirector = GetComponent<PlayableDirector>();
        m_playableDirector.playOnAwake = false;
        Duration = m_playableDirector.duration;
        CurrentPlayStatus = Status.STOPPED;
    }

    /// <summary>
    /// 继续播放
    /// </summary>
    public void Continue()
    {
        OnContinue?.Invoke(CurrentTime, CurrentPlayDirection);
        if (m_timeTween.IsActive()) m_timeTween.Play();
    }

    /// <summary>
    /// 从暂停时间点正向播放, 应用在倒播中途暂停后切换为正播
    /// </summary>
    public void ContinuePlayForwardByPausePoint()
    {
        OnContinue?.Invoke(CurrentTime, CurrentPlayDirection);
        CurrentPlayStatus = Status.PLAYING;
        CurrentPlayDirection = Direction.FORWARD;
        m_timeTween.Kill();
        RatioExecute(Duration);

    }

    /// <summary>
    /// 从暂停时间点反向播放, 应用在正播中途暂停后切换为倒播
    /// </summary>
    public void ContinuePlayBackwardByPausePoint()
    {
        OnContinue?.Invoke(CurrentTime, CurrentPlayDirection);
        CurrentPlayStatus = Status.PLAYING;
        CurrentPlayDirection = Direction.BACKWARD;
        m_timeTween.Kill();
        RatioExecute(0);
    }

    /// <summary>
    /// 从开始播放
    /// </summary>
    public void PlayForward()
    {
        OnPlay?.Invoke(CurrentTime, CurrentPlayDirection);

        m_timeTween.Kill();
        CurrentTime = 0d;
        RatioExecute(Duration);
    }

    /// <summary>
    /// 从结尾倒放
    /// </summary>
    public void PlayBackward()
    {
        OnPlay?.Invoke(CurrentTime, CurrentPlayDirection);

        m_timeTween.Kill();
        CurrentTime = Duration;
        RatioExecute(0);
    }

    /// <summary>
    /// 暂停播放
    /// </summary>
    public void Pause()
    {
        OnPause?.Invoke(CurrentTime, CurrentPlayDirection);

        m_timeTween.Pause();

    }

    /// <summary>
    /// 停止播放
    /// </summary>
    public void Stop()
    {
        OnStop?.Invoke(CurrentTime, CurrentPlayDirection);

        m_timeTween.Kill();
        CurrentTime = 0d;
        m_playableDirector.time = CurrentTime;
        m_playableDirector.Evaluate();
    }


    private void RatioExecute(double target)
    {
        // 使用DoTween最当前时间进行线性过渡
        m_timeTween = DOTween.To(() => CurrentTime, x => CurrentTime = x, target, PlaySpeed).SetSpeedBased().SetEase(Ease.Linear);
        // 做出限制避免bug
        Clamp(CurrentTime, 0d, Duration);

        m_timeTween.OnUpdate(() =>
        {
            // 直接取样
            m_playableDirector.time = CurrentTime;
            m_playableDirector.Evaluate();
        });
        m_timeTween.Play();
    }

    /// <summary>
    /// 针对Double的Clamp
    /// </summary>
    public static double Clamp(double value, double min, double max)
    {
        if (value < min)
            value = min;
        else if (value > max)
            value = max;
        return value;
    }
}

---The following is the old version----------------------------------

The project requires to control the playback status of Timeline. The official solution only deals with forward playback, and there is no interface for reverse playback.

Moreover, some of the solutions searched on the Internet use coroutines, but coroutines are difficult to deal with in the process of pausing and continuing to play.

So I just wrote one myself. Handling is rather foolish.


TimelineHelper.cs : It mainly provides two static interfaces to implement hanging TimelineDirector components

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;

namespace Tools
{
    public class TimelineHelper
    {
        /// <summary>
        /// 创建Timeline控制器
        /// </summary>
        /// <param name="director">PlayableDirector 组件</param>
        public static TimelineDirector CreateTimelineDirector(PlayableDirector director)
        {
            Debug.Assert(null != director, "null is director");
            return director.GetComponent<TimelineDirector>() ?? director.gameObject.AddComponent<TimelineDirector>();
        }

        /// <summary>
        /// 创建Timeline控制器
        /// </summary>
        /// <param name="directorPath">PlayableDirector 路径</param>
        public static TimelineDirector CreateTimelineDirector(string directorPath)
        {
            var director = GameObject.Find(directorPath);
            Debug.Assert(null != director, "null is directorPath");
            return director.GetComponent<TimelineDirector>() ?? director.gameObject.AddComponent<TimelineDirector>();
        }
    }
}

TimelineDirector.cs : Timeline related control encapsulation, no official playback API is used, only PlayableDirector time and sampling (PlayableDirector.time and PlayableDirector.Evaluate()) are used

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;

namespace Tools
{
    [RequireComponent(typeof(PlayableDirector))]
    public class TimelineDirector : MonoBehaviour
    {
        #region ENUM

        public enum Status
        {
            NULL,
            PLAYING,
            PAUSED,
            STOPPED,
        }

        public enum Direction
        {
            NULL,
            FORWARD,
            BACKWARD
        }

        #endregion

        [SerializeField]
        private PlayableDirector m_playableDirector;

        [Range(0f, 1f)]
        public float PlaySpeed = 1f;

        /// <summary>
        /// 播放模式
        /// </summary>
        public WrapMode WrapMode = WrapMode.Once;

        /// <summary>
        /// 开始播放事件, 返回时 时间点,和触发时方向
        /// </summary>
        public Action<double, Direction> OnPlay;

        /// <summary>
        /// 暂停播放事件, 返回时 时间点,和触发时方向
        /// </summary>
        public Action<double, Direction> OnPause;

        /// <summary>
        /// 停止播放事件, 返回时 时间点,和触发时方向
        /// </summary>
        public Action<double, Direction> OnStop;

        /// <summary>
        /// 继续播放事件, 返回时 时间点,和触发时方向
        /// </summary>
        public Action<double, Direction> OnContinue;

        /// <summary>
        /// Timeline长度
        /// </summary>
        public double Duration { get; private set; } = -1f;

        /// <summary>
        /// 当前播放状态
        /// </summary>
        public Status CurrentPlayStatus { get; private set; } = Status.NULL;

        /// <summary>
        /// 当前播放方向
        /// </summary>
        public Direction CurrentPlayDirection { get; private set; } = Direction.NULL;

        /// <summary>
        /// 当前播放进度
        /// </summary>
        public double CurrentTime { get; private set; } = 0d;

        /// <summary>
        /// 播放开始时间点
        /// </summary>
        private double m_timeCache = -1f;

        /// <summary>
        /// 上次运行方向
        /// </summary>
        private Direction m_prePlayedDirectionCache = Direction.NULL;

        /// <summary>
        /// 暂停时间点
        /// </summary>
        private double m_pauseTimePoint = -1f;


        private void Awake()
        {
            m_playableDirector = GetComponent<PlayableDirector>();
            Duration = m_playableDirector.duration;
            m_pauseTimePoint = 0d;
            CurrentPlayStatus = Status.STOPPED;
        }

        /// <summary>
        /// 继续播放
        /// </summary>
        public void Continue()
        {
            OnContinue?.Invoke(CurrentTime, CurrentPlayDirection);
            CurrentPlayStatus = Status.PLAYING;
            CurrentPlayDirection = m_prePlayedDirectionCache;
        }

        /// <summary>
        /// 从暂停时间点正向播放
        /// </summary>
        public void ContinuePlayForwardByPausePoint()
        {
            OnContinue?.Invoke(CurrentTime, CurrentPlayDirection);
            CurrentPlayStatus = Status.PLAYING;
            m_timeCache = m_pauseTimePoint;
            CurrentPlayDirection = Direction.FORWARD;
        }

        /// <summary>
        /// 从暂停时间点反向播放
        /// </summary>
        public void ContinuePlayBackwardByPausePoint()
        {
            OnContinue?.Invoke(CurrentTime, CurrentPlayDirection);
            CurrentPlayStatus = Status.PLAYING;
            m_timeCache = m_pauseTimePoint;
            CurrentPlayDirection = Direction.BACKWARD;
        }

        /// <summary>
        /// 从开始播放
        /// </summary>
        public void PlayForward()
        {
            OnPlay?.Invoke(CurrentTime, CurrentPlayDirection);
            m_timeCache = 0d;
            CurrentPlayStatus = Status.PLAYING;
            CurrentPlayDirection = Direction.FORWARD;
            m_prePlayedDirectionCache = Direction.NULL;
            m_pauseTimePoint = 0d;
            m_timer = 0f;
        }

        /// <summary>
        /// 从结尾倒放
        /// </summary>
        public void PlayBackward()
        {
            OnPlay?.Invoke(CurrentTime, CurrentPlayDirection);
            m_timeCache = Duration;
            CurrentPlayStatus = Status.PLAYING;
            CurrentPlayDirection = Direction.BACKWARD;
            m_prePlayedDirectionCache = Direction.NULL;
            m_pauseTimePoint = 0d;
            m_timer = 0f;
        }

        /// <summary>
        /// 暂停播放
        /// </summary>
        public void Pause()
        {
            OnPause?.Invoke(CurrentTime, CurrentPlayDirection);
            CurrentPlayStatus = Status.PAUSED;
            m_pauseTimePoint = m_playableDirector.time;
            m_timeCache = m_pauseTimePoint;
            m_prePlayedDirectionCache = CurrentPlayDirection;
            CurrentPlayDirection = Direction.NULL;
            m_timer = 0f;
        }

        /// <summary>
        /// 停止播放
        /// </summary>
        public void Stop()
        {
            OnStop?.Invoke(CurrentTime, CurrentPlayDirection);
            CurrentPlayStatus = Status.STOPPED;
            m_pauseTimePoint = 0d;
            m_timeCache = m_pauseTimePoint;
            m_prePlayedDirectionCache = Direction.NULL;
            CurrentPlayDirection = Direction.NULL;
            CurrentTime = 0d;
            m_timer = 0f;
            m_playableDirector.time = CurrentTime;
            m_playableDirector.Evaluate();
        }

        /// <summary>
        /// Lerp计时器
        /// </summary>
        private float m_timer;

        /// <summary>
        /// 继续播放时计算剩余的比例 与 Lerp计时器混合计算
        /// </summary>
        private float m_continueTimerRatio = 1f;

        private void FixedUpdate()
        {
            // 播放时触发
            if (CurrentPlayStatus.Equals(Status.PLAYING))
            {
                // Lerp计时累加
                m_timer += Time.deltaTime;
                
                // 正播
                if (CurrentPlayDirection.Equals(Direction.FORWARD))
                {
                    // 计算播放速度比例
                    m_continueTimerRatio = (float)Math.Abs(m_timeCache - Duration) / (float)Duration;
                    CurrentTime = DoubleLerp(m_timeCache, Duration, m_timer / m_continueTimerRatio * PlaySpeed);
                }
                // 倒播
                else if (CurrentPlayDirection.Equals(Direction.BACKWARD))
                {
                    m_continueTimerRatio = (float)Math.Abs(m_timeCache - 0) / (float)Duration;
                    CurrentTime = DoubleLerp(m_timeCache, 0, m_timer / m_continueTimerRatio * PlaySpeed);
                }

                // 当播放进度到1后做播放完毕处理
                if (Mathf.Clamp01(m_timer / m_continueTimerRatio * PlaySpeed).Equals(1))
                {
                    // 本次播放完毕可能时中途继续播放,还原播放比例
                    m_continueTimerRatio = 1f;

                    // 处理各个播放模式
                    switch (WrapMode)
                    {
                        // 只播放一次, 根据翻译是 播放完毕后回到初始状态
                        case WrapMode.Once:
                            Stop();
                            break;
                        // 循环播放, 方向不变,把计时器归零 Lerp继续走
                        case WrapMode.Loop:
                            m_timer = 0f;
                            break;
                        // 乒乓,方向取反 计时器归零 Lerp继续走
                        case WrapMode.PingPong:
                            CurrentPlayDirection = CurrentPlayDirection.Equals(Direction.FORWARD)
                                ? Direction.BACKWARD
                                : Direction.FORWARD;
                            m_timer = 0f;
                            break;
                        // 和Once一样了
                        case WrapMode.Default:
                            Stop();
                            break;
                        // 根绝翻译,当前方向播放完毕后保持最后的状态
                        case WrapMode.ClampForever:
                            Pause();
                            break;
                    }
                    // 因继续播放因素存在重置 时间 缓存
                    m_timeCache = CurrentPlayDirection.Equals(Direction.FORWARD) ? 0d : Duration;

                }

                // 直接取样
                m_playableDirector.time = CurrentTime;
                m_playableDirector.Evaluate();
            }
        }

        /// <summary>
        /// Lerp 没有double 特写一个
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        public double DoubleLerp(double a, double b, float t) => a + (b - a) * Mathf.Clamp01(t);

    }
}

The TimelineController.cs   test class creates a scene to make a Timeline and bind some buttons.

using System.Collections;
using System.Collections.Generic;
using Tools;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.UI;

public class TimelineController : MonoBehaviour
{

    public PlayableDirector PlayableDirector;

    public Button PlayForward;

    public Button PlayBackward;

    public Button Pause;

    public Button Stop;

    public Button Continue;

    public Button ContinueForward;

    public Button ContinueBackward;

    public TimelineDirector Director;

    public void Start()
    {
        Director = TimelineHelper.CreateTimelineDirector(PlayableDirector);

        Director.OnPlay = (t, d) =>
        {
            Debug.Log($"OnPlay time {t} dir {d}");
        };

        Director.OnPause = (t, d) =>
        {
            Debug.Log($"OnPause time {t} dir {d}");
        };

        Director.OnContinue = (t, d) =>
        {
            Debug.Log($"OnContinue time {t} dir {d}");
        };

        Director.OnStop = (t, d) =>
        {
            Debug.Log($"OnStop time {t} dir {d}");
        };

        PlayForward.onClick.AddListener(() =>
        {
            Director.PlayForward();
        });

        PlayBackward.onClick.AddListener(() =>
        {
            Director.PlayBackward();
        });

        Pause.onClick.AddListener(() =>
        {
            Director.Pause();
        });

        Stop.onClick.AddListener(() =>
        {
            Director.Stop();
        });

        Continue.onClick.AddListener(() =>
        {
            Director.Continue();
        });

        ContinueForward.onClick.AddListener(() =>
        {
            Director.ContinuePlayForwardByPausePoint();
        });

        ContinueBackward.onClick.AddListener(() =>
        {
            Director.ContinuePlayBackwardByPausePoint();
        });
    }
}

There should be room for optimization in the variable processing part. But the basic function is realized.

You can also add some parameter functions according to the official documents;

Guess you like

Origin blog.csdn.net/qq_37401396/article/details/127791478
Recommended