using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Audio; using UnityEngine.Playables; namespace UnityEngine.Timeline { // Generic evaluation callback called after all the clips have been processed internal interface ITimelineEvaluateCallback { void Evaluate(); } #if UNITY_EDITOR /// /// This Rebalancer class ensures that the interval tree structures stays balance regardless of whether the intervals inside change. /// class IntervalTreeRebalancer { private IntervalTree m_Tree; public IntervalTreeRebalancer(IntervalTree tree) { m_Tree = tree; } public bool Rebalance() { m_Tree.UpdateIntervals(); return m_Tree.dirty; } } #endif // The TimelinePlayable Playable // This is the actual runtime playable that gets evaluated as part of a playable graph. // It "compiles" a list of tracks into an IntervalTree of Runtime clips. // At each frame, it advances time, then fetches the "intersection: of various time interval // using the interval tree. // Finally, on each intersecting clip, it will calculate each clips' local time, as well as // blend weight and set them accordingly /// /// The root Playable generated by timeline. /// public class TimelinePlayable : PlayableBehaviour { private IntervalTree m_IntervalTree = new IntervalTree(); private List m_ActiveClips = new List(); private List m_CurrentListOfActiveClips; private int m_ActiveBit = 0; private List m_EvaluateCallbacks = new List(); private Dictionary m_PlayableCache = new Dictionary(); internal static bool muteAudioScrubbing = true; #if UNITY_EDITOR private IntervalTreeRebalancer m_Rebalancer; #endif /// /// Creates an instance of a Timeline /// /// The playable graph to inject the timeline. /// The list of tracks to compile /// The GameObject that initiated the compilation /// In the editor, whether the graph should account for the possibility of changing clip times /// Whether to create PlayableOutputs in the graph /// A subgraph with the playable containing a TimelinePlayable behaviour as the root public static ScriptPlayable Create(PlayableGraph graph, IEnumerable tracks, GameObject go, bool autoRebalance, bool createOutputs) { if (tracks == null) throw new ArgumentNullException("Tracks list is null", "tracks"); if (go == null) throw new ArgumentNullException("GameObject parameter is null", "go"); var playable = ScriptPlayable.Create(graph); playable.SetTraversalMode(PlayableTraversalMode.Passthrough); var sequence = playable.GetBehaviour(); sequence.Compile(graph, playable, tracks, go, autoRebalance, createOutputs); return playable; } /// /// Compiles the subgraph of this timeline /// /// The playable graph to inject the timeline. /// /// The list of tracks to compile /// The GameObject that initiated the compilation /// In the editor, whether the graph should account for the possibility of changing clip times /// Whether to create PlayableOutputs in the graph public void Compile(PlayableGraph graph, Playable timelinePlayable, IEnumerable tracks, GameObject go, bool autoRebalance, bool createOutputs) { if (tracks == null) throw new ArgumentNullException("Tracks list is null", "tracks"); if (go == null) throw new ArgumentNullException("GameObject parameter is null", "go"); var outputTrackList = new List(tracks); var maximumNumberOfIntersections = outputTrackList.Count * 2 + outputTrackList.Count; // worse case: 2 overlapping clips per track + each track m_CurrentListOfActiveClips = new List(maximumNumberOfIntersections); m_ActiveClips = new List(maximumNumberOfIntersections); m_EvaluateCallbacks.Clear(); m_PlayableCache.Clear(); CompileTrackList(graph, timelinePlayable, outputTrackList, go, createOutputs); #if UNITY_EDITOR if (autoRebalance) { m_Rebalancer = new IntervalTreeRebalancer(m_IntervalTree); } #endif } private void CompileTrackList(PlayableGraph graph, Playable timelinePlayable, IEnumerable tracks, GameObject go, bool createOutputs) { foreach (var track in tracks) { if (!track.IsCompilable()) continue; if (!m_PlayableCache.ContainsKey(track)) { track.SortClips(); CreateTrackPlayable(graph, timelinePlayable, track, go, createOutputs); } } } void CreateTrackOutput(PlayableGraph graph, TrackAsset track, GameObject go, Playable playable, int port) { if (track.isSubTrack) return; var bindings = track.outputs; foreach (var binding in bindings) { var playableOutput = binding.CreateOutput(graph); playableOutput.SetReferenceObject(binding.sourceObject); playableOutput.SetSourcePlayable(playable, port); playableOutput.SetWeight(1.0f); // only apply this on our animation track if (track as AnimationTrack != null) { EvaluateWeightsForAnimationPlayableOutput(track, (AnimationPlayableOutput)playableOutput); #if UNITY_EDITOR if (!Application.isPlaying) EvaluateAnimationPreviewUpdateCallback(track, (AnimationPlayableOutput)playableOutput); #endif } if (playableOutput.IsPlayableOutputOfType()) ((AudioPlayableOutput)playableOutput).SetEvaluateOnSeek(!muteAudioScrubbing); // If the track is the timeline marker track, assume binding is the PlayableDirector if (track.timelineAsset.markerTrack == track) { var director = go.GetComponent(); playableOutput.SetUserData(director); foreach (var c in go.GetComponents()) { playableOutput.AddNotificationReceiver(c); } } } } void EvaluateWeightsForAnimationPlayableOutput(TrackAsset track, AnimationPlayableOutput animOutput) { m_EvaluateCallbacks.Add(new AnimationOutputWeightProcessor(animOutput)); } void EvaluateAnimationPreviewUpdateCallback(TrackAsset track, AnimationPlayableOutput animOutput) { m_EvaluateCallbacks.Add(new AnimationPreviewUpdateCallback(animOutput)); } private static Playable CreatePlayableGraph(PlayableGraph graph, TrackAsset asset, GameObject go, IntervalTree tree, Playable timelinePlayable) { return asset.CreatePlayableGraph(graph, go, tree, timelinePlayable); } private Playable CreateTrackPlayable(PlayableGraph graph, Playable timelinePlayable, TrackAsset track, GameObject go, bool createOutputs) { if (!track.IsCompilable()) // where parents are not compilable (group tracks) return timelinePlayable; Playable playable; if (m_PlayableCache.TryGetValue(track, out playable)) return playable; if (track.name == "root") return timelinePlayable; TrackAsset parentActor = track.parent as TrackAsset; var parentPlayable = parentActor != null ? CreateTrackPlayable(graph, timelinePlayable, parentActor, go, createOutputs) : timelinePlayable; var actorPlayable = CreatePlayableGraph(graph, track, go, m_IntervalTree, timelinePlayable); bool connected = false; if (!actorPlayable.IsValid()) { // if a track says it's compilable, but returns Playable.Null, that can screw up the whole graph. 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"); } // Special case for animation tracks if (parentPlayable.IsValid() && actorPlayable.IsValid()) { int port = parentPlayable.GetInputCount(); parentPlayable.SetInputCount(port + 1); connected = graph.Connect(actorPlayable, 0, parentPlayable, port); parentPlayable.SetInputWeight(port, 1.0f); } if (createOutputs && connected) { CreateTrackOutput(graph, track, go, parentPlayable, parentPlayable.GetInputCount() - 1); } CacheTrack(track, actorPlayable, connected ? (parentPlayable.GetInputCount() - 1) : -1, parentPlayable); return actorPlayable; } /// /// Overridden to handle synchronizing time on the timeline instance. /// /// 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 UNITY_EDITOR if (m_Rebalancer != null) m_Rebalancer.Rebalance(); #endif // force seek if we are being evaluated // or if our time has jumped. This is used to // resynchronize Evaluate(playable, info); } private void Evaluate(Playable playable, FrameData frameData) { if (m_IntervalTree == null) return; double localTime = playable.GetTime(); m_ActiveBit = m_ActiveBit == 0 ? 1 : 0; m_CurrentListOfActiveClips.Clear(); m_IntervalTree.IntersectsWith(DiscreteTime.GetNearestTick(localTime), m_CurrentListOfActiveClips); foreach (var c in m_CurrentListOfActiveClips) { c.intervalBit = m_ActiveBit; if (frameData.timeLooped) c.Reset(); } // all previously active clips having a different intervalBit flag are not // in the current intersection, therefore are considered becoming disabled at this frame var timelineEnd = playable.GetDuration(); foreach (var c in m_ActiveClips) { if (c.intervalBit != m_ActiveBit) { var clipEnd = (double)DiscreteTime.FromTicks(c.intervalEnd); var time = frameData.timeLooped ? Math.Min(clipEnd, timelineEnd) : Math.Min(localTime, clipEnd); c.EvaluateAt(time, frameData); c.enable = false; } } m_ActiveClips.Clear(); // case 998642 - don't use m_ActiveClips.AddRange, as in 4.6 .Net scripting it causes GC allocs for (var a = 0; a < m_CurrentListOfActiveClips.Count; a++) { m_CurrentListOfActiveClips[a].EvaluateAt(localTime, frameData); m_ActiveClips.Add(m_CurrentListOfActiveClips[a]); } int count = m_EvaluateCallbacks.Count; for (int i = 0; i < count; i++) { m_EvaluateCallbacks[i].Evaluate(); } } private void CacheTrack(TrackAsset track, Playable playable, int port, Playable parent) { m_PlayableCache[track] = playable; } //necessary to build on AOT platforms static void ForAOTCompilationOnly() { new List.Entry>(); } } }