TimelinePlayable.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.Animations;
  5. using UnityEngine.Audio;
  6. using UnityEngine.Playables;
  7. namespace UnityEngine.Timeline
  8. {
  9. // Generic evaluation callback called after all the clips have been processed
  10. internal interface ITimelineEvaluateCallback
  11. {
  12. void Evaluate();
  13. }
  14. #if UNITY_EDITOR
  15. /// <summary>
  16. /// This Rebalancer class ensures that the interval tree structures stays balance regardless of whether the intervals inside change.
  17. /// </summary>
  18. class IntervalTreeRebalancer
  19. {
  20. private IntervalTree<RuntimeElement> m_Tree;
  21. public IntervalTreeRebalancer(IntervalTree<RuntimeElement> tree)
  22. {
  23. m_Tree = tree;
  24. }
  25. public bool Rebalance()
  26. {
  27. m_Tree.UpdateIntervals();
  28. return m_Tree.dirty;
  29. }
  30. }
  31. #endif
  32. // The TimelinePlayable Playable
  33. // This is the actual runtime playable that gets evaluated as part of a playable graph.
  34. // It "compiles" a list of tracks into an IntervalTree of Runtime clips.
  35. // At each frame, it advances time, then fetches the "intersection: of various time interval
  36. // using the interval tree.
  37. // Finally, on each intersecting clip, it will calculate each clips' local time, as well as
  38. // blend weight and set them accordingly
  39. /// <summary>
  40. /// The root Playable generated by timeline.
  41. /// </summary>
  42. public class TimelinePlayable : PlayableBehaviour
  43. {
  44. private IntervalTree<RuntimeElement> m_IntervalTree = new IntervalTree<RuntimeElement>();
  45. private List<RuntimeElement> m_ActiveClips = new List<RuntimeElement>();
  46. private List<RuntimeElement> m_CurrentListOfActiveClips;
  47. private int m_ActiveBit = 0;
  48. private List<ITimelineEvaluateCallback> m_EvaluateCallbacks = new List<ITimelineEvaluateCallback>();
  49. private Dictionary<TrackAsset, Playable> m_PlayableCache = new Dictionary<TrackAsset, Playable>();
  50. internal static bool muteAudioScrubbing = true;
  51. #if UNITY_EDITOR
  52. private IntervalTreeRebalancer m_Rebalancer;
  53. #endif
  54. /// <summary>
  55. /// Creates an instance of a Timeline
  56. /// </summary>
  57. /// <param name="graph">The playable graph to inject the timeline.</param>
  58. /// <param name="tracks">The list of tracks to compile</param>
  59. /// <param name="go">The GameObject that initiated the compilation</param>
  60. /// <param name="autoRebalance">In the editor, whether the graph should account for the possibility of changing clip times</param>
  61. /// <param name="createOutputs">Whether to create PlayableOutputs in the graph</param>
  62. /// <returns>A subgraph with the playable containing a TimelinePlayable behaviour as the root</returns>
  63. public static ScriptPlayable<TimelinePlayable> Create(PlayableGraph graph, IEnumerable<TrackAsset> tracks, GameObject go, bool autoRebalance, bool createOutputs)
  64. {
  65. if (tracks == null)
  66. throw new ArgumentNullException("Tracks list is null", "tracks");
  67. if (go == null)
  68. throw new ArgumentNullException("GameObject parameter is null", "go");
  69. var playable = ScriptPlayable<TimelinePlayable>.Create(graph);
  70. playable.SetTraversalMode(PlayableTraversalMode.Passthrough);
  71. var sequence = playable.GetBehaviour();
  72. sequence.Compile(graph, playable, tracks, go, autoRebalance, createOutputs);
  73. return playable;
  74. }
  75. /// <summary>
  76. /// Compiles the subgraph of this timeline
  77. /// </summary>
  78. /// <param name="graph">The playable graph to inject the timeline.</param>
  79. /// <param name="timelinePlayable"></param>
  80. /// <param name="tracks">The list of tracks to compile</param>
  81. /// <param name="go">The GameObject that initiated the compilation</param>
  82. /// <param name="autoRebalance">In the editor, whether the graph should account for the possibility of changing clip times</param>
  83. /// <param name="createOutputs">Whether to create PlayableOutputs in the graph</param>
  84. public void Compile(PlayableGraph graph, Playable timelinePlayable, IEnumerable<TrackAsset> tracks, GameObject go, bool autoRebalance, bool createOutputs)
  85. {
  86. if (tracks == null)
  87. throw new ArgumentNullException("Tracks list is null", "tracks");
  88. if (go == null)
  89. throw new ArgumentNullException("GameObject parameter is null", "go");
  90. var outputTrackList = new List<TrackAsset>(tracks);
  91. var maximumNumberOfIntersections = outputTrackList.Count * 2 + outputTrackList.Count; // worse case: 2 overlapping clips per track + each track
  92. m_CurrentListOfActiveClips = new List<RuntimeElement>(maximumNumberOfIntersections);
  93. m_ActiveClips = new List<RuntimeElement>(maximumNumberOfIntersections);
  94. m_EvaluateCallbacks.Clear();
  95. m_PlayableCache.Clear();
  96. CompileTrackList(graph, timelinePlayable, outputTrackList, go, createOutputs);
  97. #if UNITY_EDITOR
  98. if (autoRebalance)
  99. {
  100. m_Rebalancer = new IntervalTreeRebalancer(m_IntervalTree);
  101. }
  102. #endif
  103. }
  104. private void CompileTrackList(PlayableGraph graph, Playable timelinePlayable, IEnumerable<TrackAsset> tracks, GameObject go, bool createOutputs)
  105. {
  106. foreach (var track in tracks)
  107. {
  108. if (!track.IsCompilable())
  109. continue;
  110. if (!m_PlayableCache.ContainsKey(track))
  111. {
  112. track.SortClips();
  113. CreateTrackPlayable(graph, timelinePlayable, track, go, createOutputs);
  114. }
  115. }
  116. }
  117. void CreateTrackOutput(PlayableGraph graph, TrackAsset track, GameObject go, Playable playable, int port)
  118. {
  119. if (track.isSubTrack)
  120. return;
  121. var bindings = track.outputs;
  122. foreach (var binding in bindings)
  123. {
  124. var playableOutput = binding.CreateOutput(graph);
  125. playableOutput.SetReferenceObject(binding.sourceObject);
  126. playableOutput.SetSourcePlayable(playable, port);
  127. playableOutput.SetWeight(1.0f);
  128. // only apply this on our animation track
  129. if (track as AnimationTrack != null)
  130. {
  131. EvaluateWeightsForAnimationPlayableOutput(track, (AnimationPlayableOutput)playableOutput);
  132. #if UNITY_EDITOR
  133. if (!Application.isPlaying)
  134. EvaluateAnimationPreviewUpdateCallback(track, (AnimationPlayableOutput)playableOutput);
  135. #endif
  136. }
  137. if (playableOutput.IsPlayableOutputOfType<AudioPlayableOutput>())
  138. ((AudioPlayableOutput)playableOutput).SetEvaluateOnSeek(!muteAudioScrubbing);
  139. // If the track is the timeline marker track, assume binding is the PlayableDirector
  140. if (track.timelineAsset.markerTrack == track)
  141. {
  142. var director = go.GetComponent<PlayableDirector>();
  143. playableOutput.SetUserData(director);
  144. foreach (var c in go.GetComponents<INotificationReceiver>())
  145. {
  146. playableOutput.AddNotificationReceiver(c);
  147. }
  148. }
  149. }
  150. }
  151. void EvaluateWeightsForAnimationPlayableOutput(TrackAsset track, AnimationPlayableOutput animOutput)
  152. {
  153. m_EvaluateCallbacks.Add(new AnimationOutputWeightProcessor(animOutput));
  154. }
  155. void EvaluateAnimationPreviewUpdateCallback(TrackAsset track, AnimationPlayableOutput animOutput)
  156. {
  157. m_EvaluateCallbacks.Add(new AnimationPreviewUpdateCallback(animOutput));
  158. }
  159. private static Playable CreatePlayableGraph(PlayableGraph graph, TrackAsset asset, GameObject go, IntervalTree<RuntimeElement> tree, Playable timelinePlayable)
  160. {
  161. return asset.CreatePlayableGraph(graph, go, tree, timelinePlayable);
  162. }
  163. private Playable CreateTrackPlayable(PlayableGraph graph, Playable timelinePlayable, TrackAsset track, GameObject go, bool createOutputs)
  164. {
  165. if (!track.IsCompilable()) // where parents are not compilable (group tracks)
  166. return timelinePlayable;
  167. Playable playable;
  168. if (m_PlayableCache.TryGetValue(track, out playable))
  169. return playable;
  170. if (track.name == "root")
  171. return timelinePlayable;
  172. TrackAsset parentActor = track.parent as TrackAsset;
  173. var parentPlayable = parentActor != null ? CreateTrackPlayable(graph, timelinePlayable, parentActor, go, createOutputs) : timelinePlayable;
  174. var actorPlayable = CreatePlayableGraph(graph, track, go, m_IntervalTree, timelinePlayable);
  175. bool connected = false;
  176. if (!actorPlayable.IsValid())
  177. {
  178. // if a track says it's compilable, but returns Playable.Null, that can screw up the whole graph.
  179. throw new InvalidOperationException(track.name + "(" + track.GetType() + ") did not produce a valid playable. Use the compilable property to indicate whether the track is valid for processing");
  180. }
  181. // Special case for animation tracks
  182. if (parentPlayable.IsValid() && actorPlayable.IsValid())
  183. {
  184. int port = parentPlayable.GetInputCount();
  185. parentPlayable.SetInputCount(port + 1);
  186. connected = graph.Connect(actorPlayable, 0, parentPlayable, port);
  187. parentPlayable.SetInputWeight(port, 1.0f);
  188. }
  189. if (createOutputs && connected)
  190. {
  191. CreateTrackOutput(graph, track, go, parentPlayable, parentPlayable.GetInputCount() - 1);
  192. }
  193. CacheTrack(track, actorPlayable, connected ? (parentPlayable.GetInputCount() - 1) : -1, parentPlayable);
  194. return actorPlayable;
  195. }
  196. /// <summary>
  197. /// Overridden to handle synchronizing time on the timeline instance.
  198. /// </summary>
  199. /// <param name="playable">The Playable that owns the current PlayableBehaviour.</param>
  200. /// <param name="info">A FrameData structure that contains information about the current frame context.</param>
  201. public override void PrepareFrame(Playable playable, FrameData info)
  202. {
  203. #if UNITY_EDITOR
  204. if (m_Rebalancer != null)
  205. m_Rebalancer.Rebalance();
  206. #endif
  207. // force seek if we are being evaluated
  208. // or if our time has jumped. This is used to
  209. // resynchronize
  210. Evaluate(playable, info);
  211. }
  212. private void Evaluate(Playable playable, FrameData frameData)
  213. {
  214. if (m_IntervalTree == null)
  215. return;
  216. double localTime = playable.GetTime();
  217. m_ActiveBit = m_ActiveBit == 0 ? 1 : 0;
  218. m_CurrentListOfActiveClips.Clear();
  219. m_IntervalTree.IntersectsWith(DiscreteTime.GetNearestTick(localTime), m_CurrentListOfActiveClips);
  220. foreach (var c in m_CurrentListOfActiveClips)
  221. {
  222. c.intervalBit = m_ActiveBit;
  223. if (frameData.timeLooped)
  224. c.Reset();
  225. }
  226. // all previously active clips having a different intervalBit flag are not
  227. // in the current intersection, therefore are considered becoming disabled at this frame
  228. var timelineEnd = playable.GetDuration();
  229. foreach (var c in m_ActiveClips)
  230. {
  231. if (c.intervalBit != m_ActiveBit)
  232. {
  233. var clipEnd = (double)DiscreteTime.FromTicks(c.intervalEnd);
  234. var time = frameData.timeLooped ? Math.Min(clipEnd, timelineEnd) : Math.Min(localTime, clipEnd);
  235. c.EvaluateAt(time, frameData);
  236. c.enable = false;
  237. }
  238. }
  239. m_ActiveClips.Clear();
  240. // case 998642 - don't use m_ActiveClips.AddRange, as in 4.6 .Net scripting it causes GC allocs
  241. for (var a = 0; a < m_CurrentListOfActiveClips.Count; a++)
  242. {
  243. m_CurrentListOfActiveClips[a].EvaluateAt(localTime, frameData);
  244. m_ActiveClips.Add(m_CurrentListOfActiveClips[a]);
  245. }
  246. int count = m_EvaluateCallbacks.Count;
  247. for (int i = 0; i < count; i++)
  248. {
  249. m_EvaluateCallbacks[i].Evaluate();
  250. }
  251. }
  252. private void CacheTrack(TrackAsset track, Playable playable, int port, Playable parent)
  253. {
  254. m_PlayableCache[track] = playable;
  255. }
  256. //necessary to build on AOT platforms
  257. static void ForAOTCompilationOnly()
  258. {
  259. new List<IntervalTree<RuntimeElement>.Entry>();
  260. }
  261. }
  262. }