2023.5.30更新

unity 状态控制机里面循环动画播完 unity timeline循环播放_Time

 针对@欢乐的小胖子小伙伴提出的bug做了一次修改。

直接替换 TimelineDirector.cs 即可:

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
        CurrentTime = 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;
    }
}

---以下为旧版本-----------------------------------

项目要求控制Timeline的播放状态,官方给出的方案只有正播的处理,并没有倒播的接口。

而且网上搜索的一些方案都是使用协程,但是协程在处理中途暂停继续播放上比较难处理。

所以自己随便写了一个。处理上比较傻瓜。


TimelineHelper.cs :主要是提供了两个静态接口,用来实现挂TimelineDirector组件

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

namespace Tools
{
    public static class PlayableExpansion
    {
        /// <summary>
        /// 为director添加自定义控制器
        /// </summary>
        /// <param name="director"></param>
        /// <returns></returns>
        public static PlayableController AddPlayableController(this PlayableDirector director)
        {
            return director.gameObject.AddComponent<TimelineDirector>();
        }

        /// <summary>
        /// 为obj添加自定义控制器
        /// </summary>
        /// <param name="director"></param>
        /// <returns></returns>
        public static PlayableController AddPlayableController(this GameObject obj)
        {
            return obj.AddComponent<TimelineDirector>();
        }
    }


    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的相关控制封装,没有用官方播放的API,只用到了PlayableDirector时间和采样(PlayableDirector.time和PlayableDirector.Evaluate())

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);

    }
}

TimelineController.cs  测试类创建一个场景自己制作一个Timeline, 绑定一些按钮。

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();
        });
    }
}

变量处理部分应该还有优化的余地。但是基本功能实现了。