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