A game about forced loneliness, made by TACStudios
at master 303 lines 13 kB view raw
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}