277 lines
11 KiB
C#
277 lines
11 KiB
C#
|
using System;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.Playables;
|
||
|
|
||
|
namespace UnityEngine.Timeline
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Playable Behaviour used to control a PlayableDirector.
|
||
|
/// </summary>
|
||
|
/// <remarks>
|
||
|
/// This playable is used to control other PlayableDirector components from a Timeline sequence.
|
||
|
/// </remarks>
|
||
|
public class DirectorControlPlayable : PlayableBehaviour
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Represents the action taken when the DirectorControlPlayable is stopped.
|
||
|
/// </summary>
|
||
|
public enum PauseAction
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Stop the <see cref="PlayableDirector"/> on pause.
|
||
|
/// </summary>
|
||
|
StopDirector,
|
||
|
|
||
|
/// <summary>
|
||
|
/// Pause the <see cref="PlayableDirector"/> on pause.
|
||
|
/// </summary>
|
||
|
PauseDirector,
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// The PlayableDirector being controlled by this PlayableBehaviour
|
||
|
/// </summary>
|
||
|
public PlayableDirector director;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Sets the action to perform when the playable is paused. <see cref="PauseAction"/>
|
||
|
/// </summary>
|
||
|
public PauseAction pauseAction;
|
||
|
|
||
|
bool m_SyncTime = false;
|
||
|
double m_AssetDuration = double.MaxValue;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a Playable with a DirectorControlPlayable attached
|
||
|
/// </summary>
|
||
|
/// <param name="graph">The graph to inject the playable into</param>
|
||
|
/// <param name="director">The director to control</param>
|
||
|
/// <returns>Returns a Playable with a DirectorControlPlayable attached</returns>
|
||
|
public static ScriptPlayable<DirectorControlPlayable> Create(PlayableGraph graph, PlayableDirector director)
|
||
|
{
|
||
|
if (director == null)
|
||
|
return ScriptPlayable<DirectorControlPlayable>.Null;
|
||
|
|
||
|
var handle = ScriptPlayable<DirectorControlPlayable>.Create(graph);
|
||
|
handle.GetBehaviour().director = director;
|
||
|
|
||
|
#if UNITY_EDITOR
|
||
|
if (!Application.isPlaying && UnityEditor.PrefabUtility.IsPartOfPrefabInstance(director))
|
||
|
UnityEditor.PrefabUtility.prefabInstanceUpdated += handle.GetBehaviour().OnPrefabUpdated;
|
||
|
#endif
|
||
|
|
||
|
return handle;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// This function is called when this PlayableBehaviour is destroyed.
|
||
|
/// </summary>
|
||
|
/// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
|
||
|
public override void OnPlayableDestroy(Playable playable)
|
||
|
{
|
||
|
#if UNITY_EDITOR
|
||
|
if (!Application.isPlaying)
|
||
|
UnityEditor.PrefabUtility.prefabInstanceUpdated -= OnPrefabUpdated;
|
||
|
#endif
|
||
|
if (director != null && director.playableAsset != null)
|
||
|
director.Stop();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// This function is called during the PrepareFrame phase of the PlayableGraph.
|
||
|
/// </summary>
|
||
|
/// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
|
||
|
/// <param name="info">A FrameData structure that contains information about the current frame context.</param>
|
||
|
public override void PrepareFrame(Playable playable, FrameData info)
|
||
|
{
|
||
|
if (director == null || !director.isActiveAndEnabled || director.playableAsset == null)
|
||
|
return;
|
||
|
|
||
|
// resync the time on an evaluate or a time jump (caused by loops, or some setTime calls)
|
||
|
m_SyncTime |= (info.evaluationType == FrameData.EvaluationType.Evaluate) ||
|
||
|
DetectDiscontinuity(playable, info);
|
||
|
|
||
|
SyncSpeed(info.effectiveSpeed);
|
||
|
SyncStart(playable.GetGraph(), playable.GetTime());
|
||
|
#if !UNITY_2021_2_OR_NEWER
|
||
|
SyncStop(playable.GetGraph(), playable.GetTime());
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// This function is called when the Playable play state is changed to Playables.PlayState.Playing.
|
||
|
/// </summary>
|
||
|
/// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
|
||
|
/// <param name="info">A FrameData structure that contains information about the current frame context.</param>
|
||
|
public override void OnBehaviourPlay(Playable playable, FrameData info)
|
||
|
{
|
||
|
m_SyncTime = true;
|
||
|
|
||
|
if (director != null && director.playableAsset != null)
|
||
|
m_AssetDuration = director.playableAsset.duration;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// This function is called when the Playable play state is changed to PlayState.Paused.
|
||
|
/// </summary>
|
||
|
/// <param name="playable">The playable this behaviour is attached to.</param>
|
||
|
/// <param name="info">A FrameData structure that contains information about the current frame context.</param>
|
||
|
public override void OnBehaviourPause(Playable playable, FrameData info)
|
||
|
{
|
||
|
if (director != null && director.playableAsset != null)
|
||
|
{
|
||
|
if (info.effectivePlayState == PlayState.Playing ||
|
||
|
info.effectivePlayState == PlayState.Paused && pauseAction == PauseAction.PauseDirector) // graph was paused
|
||
|
{
|
||
|
director.Pause();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
director.Stop();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// This function is called during the ProcessFrame phase of the PlayableGraph.
|
||
|
/// </summary>
|
||
|
/// <param name="playable">The playable this behaviour is attached to.</param>
|
||
|
/// <param name="info">A FrameData structure that contains information about the current frame context.</param>
|
||
|
/// <param name="playerData">unused</param>
|
||
|
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
|
||
|
{
|
||
|
if (director == null || !director.isActiveAndEnabled || director.playableAsset == null)
|
||
|
return;
|
||
|
|
||
|
if (m_SyncTime || DetectOutOfSync(playable))
|
||
|
{
|
||
|
UpdateTime(playable);
|
||
|
if (director.playableGraph.IsValid())
|
||
|
{
|
||
|
director.playableGraph.Evaluate();
|
||
|
#if TIMELINE_FRAMEACCURATE
|
||
|
director.playableGraph.SynchronizeEvaluation(playable.GetGraph());
|
||
|
#endif
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
director.Evaluate();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
m_SyncTime = false;
|
||
|
#if UNITY_2021_2_OR_NEWER
|
||
|
SyncStop(playable.GetGraph(), playable.GetTime());
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
#if UNITY_EDITOR
|
||
|
void OnPrefabUpdated(GameObject go)
|
||
|
{
|
||
|
// When the prefab asset is updated, we rebuild the graph to reflect the changes in editor
|
||
|
if (UnityEditor.PrefabUtility.GetRootGameObject(director) == go)
|
||
|
director.RebuildGraph();
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
void SyncSpeed(double speed)
|
||
|
{
|
||
|
if (director.playableGraph.IsValid())
|
||
|
{
|
||
|
int roots = director.playableGraph.GetRootPlayableCount();
|
||
|
for (int i = 0; i < roots; i++)
|
||
|
{
|
||
|
var rootPlayable = director.playableGraph.GetRootPlayable(i);
|
||
|
if (rootPlayable.IsValid())
|
||
|
{
|
||
|
rootPlayable.SetSpeed(speed);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void SyncStart(PlayableGraph graph, double time)
|
||
|
{
|
||
|
if (director.state == PlayState.Playing
|
||
|
|| !graph.IsPlaying()
|
||
|
|| (director.extrapolationMode == DirectorWrapMode.None && time > m_AssetDuration))
|
||
|
return;
|
||
|
#if TIMELINE_FRAMEACCURATE
|
||
|
if (graph.IsMatchFrameRateEnabled())
|
||
|
director.Play(graph.GetFrameRate());
|
||
|
else
|
||
|
director.Play();
|
||
|
#else
|
||
|
director.Play();
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void SyncStop(PlayableGraph graph, double time)
|
||
|
{
|
||
|
if (director.state == PlayState.Paused
|
||
|
|| (graph.IsPlaying() && (director.extrapolationMode != DirectorWrapMode.None || time < m_AssetDuration)))
|
||
|
return;
|
||
|
if (director.state == PlayState.Paused)
|
||
|
return;
|
||
|
|
||
|
bool expectedFinished = director.extrapolationMode == DirectorWrapMode.None && time > m_AssetDuration;
|
||
|
if (expectedFinished || !graph.IsPlaying())
|
||
|
director.Pause();
|
||
|
}
|
||
|
|
||
|
bool DetectDiscontinuity(Playable playable, FrameData info)
|
||
|
{
|
||
|
return Math.Abs(playable.GetTime() - playable.GetPreviousTime() - info.m_DeltaTime * info.m_EffectiveSpeed) > DiscreteTime.tickValue;
|
||
|
}
|
||
|
|
||
|
bool DetectOutOfSync(Playable playable)
|
||
|
{
|
||
|
double expectedTime = playable.GetTime();
|
||
|
if (playable.GetTime() >= m_AssetDuration)
|
||
|
{
|
||
|
switch (director.extrapolationMode)
|
||
|
{
|
||
|
case DirectorWrapMode.None:
|
||
|
expectedTime = m_AssetDuration;
|
||
|
break;
|
||
|
case DirectorWrapMode.Hold:
|
||
|
expectedTime = m_AssetDuration;
|
||
|
break;
|
||
|
case DirectorWrapMode.Loop:
|
||
|
expectedTime %= m_AssetDuration;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!Mathf.Approximately((float)expectedTime, (float)director.time))
|
||
|
{
|
||
|
#if UNITY_EDITOR
|
||
|
double lastDelta = playable.GetTime() - playable.GetPreviousTime();
|
||
|
if (UnityEditor.Unsupported.IsDeveloperBuild())
|
||
|
Debug.LogWarningFormat("Internal Warning - Control track desync detected on {2} ({0:F10} vs {1:F10} with delta {3:F10}). Time will be resynchronized. Known to happen with nested control tracks", playable.GetTime(), director.time, director.name, lastDelta);
|
||
|
#endif
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// We need to handle loop modes explicitly since we are setting the time directly
|
||
|
void UpdateTime(Playable playable)
|
||
|
{
|
||
|
double duration = Math.Max(0.1, director.playableAsset.duration);
|
||
|
switch (director.extrapolationMode)
|
||
|
{
|
||
|
case DirectorWrapMode.Hold:
|
||
|
director.time = Math.Min(duration, Math.Max(0, playable.GetTime()));
|
||
|
break;
|
||
|
case DirectorWrapMode.Loop:
|
||
|
director.time = Math.Max(0, playable.GetTime() % duration);
|
||
|
break;
|
||
|
case DirectorWrapMode.None:
|
||
|
director.time = Math.Min(duration, Math.Max(0, playable.GetTime()));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|