2023.5.30更新
针对@欢乐的小胖子小伙伴提出的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();
});
}
}
变量处理部分应该还有优化的余地。但是基本功能实现了。