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