using System;
using UnityEngine;
using UnityEngine.Playables;
namespace UnityEngine.Timeline
{
///
/// Playable Behaviour used to control a PlayableDirector.
///
///
/// This playable is used to control other PlayableDirector components from a Timeline sequence.
///
public class DirectorControlPlayable : PlayableBehaviour
{
///
/// Represents the action taken when the DirectorControlPlayable is stopped.
///
public enum PauseAction
{
///
/// Stop the on pause.
///
StopDirector,
///
/// Pause the on pause.
///
PauseDirector,
}
///
/// The PlayableDirector being controlled by this PlayableBehaviour
///
public PlayableDirector director;
///
/// Sets the action to perform when the playable is paused.
///
public PauseAction pauseAction;
bool m_SyncTime = false;
double m_AssetDuration = double.MaxValue;
///
/// Creates a Playable with a DirectorControlPlayable attached
///
/// The graph to inject the playable into
/// The director to control
/// Returns a Playable with a DirectorControlPlayable attached
public static ScriptPlayable Create(PlayableGraph graph, PlayableDirector director)
{
if (director == null)
return ScriptPlayable.Null;
var handle = ScriptPlayable.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;
}
///
/// This function is called when this PlayableBehaviour is destroyed.
///
/// The Playable that owns the current PlayableBehaviour.
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();
}
///
/// This function is called during the PrepareFrame phase of the PlayableGraph.
///
/// The Playable that owns the current PlayableBehaviour.
/// A FrameData structure that contains information about the current frame context.
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
}
///
/// This function is called when the Playable play state is changed to Playables.PlayState.Playing.
///
/// The Playable that owns the current PlayableBehaviour.
/// A FrameData structure that contains information about the current frame context.
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
m_SyncTime = true;
if (director != null && director.playableAsset != null)
m_AssetDuration = director.playableAsset.duration;
}
///
/// This function is called when the Playable play state is changed to PlayState.Paused.
///
/// The playable this behaviour is attached to.
/// A FrameData structure that contains information about the current frame context.
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();
}
}
}
///
/// This function is called during the ProcessFrame phase of the PlayableGraph.
///
/// The playable this behaviour is attached to.
/// A FrameData structure that contains information about the current frame context.
/// unused
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;
}
}
}
}