You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
13 KiB
303 lines
13 KiB
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 |
|
/// <summary> |
|
/// This Rebalancer class ensures that the interval tree structures stays balance regardless of whether the intervals inside change. |
|
/// </summary> |
|
class IntervalTreeRebalancer |
|
{ |
|
private IntervalTree<RuntimeElement> m_Tree; |
|
public IntervalTreeRebalancer(IntervalTree<RuntimeElement> 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 |
|
|
|
|
|
/// <summary> |
|
/// The root Playable generated by timeline. |
|
/// </summary> |
|
public class TimelinePlayable : PlayableBehaviour |
|
{ |
|
private IntervalTree<RuntimeElement> m_IntervalTree = new IntervalTree<RuntimeElement>(); |
|
private List<RuntimeElement> m_ActiveClips = new List<RuntimeElement>(); |
|
private List<RuntimeElement> m_CurrentListOfActiveClips; |
|
private int m_ActiveBit = 0; |
|
|
|
private List<ITimelineEvaluateCallback> m_EvaluateCallbacks = new List<ITimelineEvaluateCallback>(); |
|
|
|
private Dictionary<TrackAsset, Playable> m_PlayableCache = new Dictionary<TrackAsset, Playable>(); |
|
|
|
internal static bool muteAudioScrubbing = true; |
|
|
|
#if UNITY_EDITOR |
|
private IntervalTreeRebalancer m_Rebalancer; |
|
internal static event Action<Playable> playableLooped; |
|
#endif |
|
/// <summary> |
|
/// Creates an instance of a Timeline |
|
/// </summary> |
|
/// <param name="graph">The playable graph to inject the timeline.</param> |
|
/// <param name="tracks">The list of tracks to compile</param> |
|
/// <param name="go">The GameObject that initiated the compilation</param> |
|
/// <param name="autoRebalance">In the editor, whether the graph should account for the possibility of changing clip times</param> |
|
/// <param name="createOutputs">Whether to create PlayableOutputs in the graph</param> |
|
/// <returns>A subgraph with the playable containing a TimelinePlayable behaviour as the root</returns> |
|
public static ScriptPlayable<TimelinePlayable> Create(PlayableGraph graph, IEnumerable<TrackAsset> 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<TimelinePlayable>.Create(graph); |
|
playable.SetTraversalMode(PlayableTraversalMode.Passthrough); |
|
var sequence = playable.GetBehaviour(); |
|
sequence.Compile(graph, playable, tracks, go, autoRebalance, createOutputs); |
|
return playable; |
|
} |
|
|
|
/// <summary> |
|
/// Compiles the subgraph of this timeline |
|
/// </summary> |
|
/// <param name="graph">The playable graph to inject the timeline.</param> |
|
/// <param name="timelinePlayable"></param> |
|
/// <param name="tracks">The list of tracks to compile</param> |
|
/// <param name="go">The GameObject that initiated the compilation</param> |
|
/// <param name="autoRebalance">In the editor, whether the graph should account for the possibility of changing clip times</param> |
|
/// <param name="createOutputs">Whether to create PlayableOutputs in the graph</param> |
|
public void Compile(PlayableGraph graph, Playable timelinePlayable, IEnumerable<TrackAsset> 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<TrackAsset>(tracks); |
|
var maximumNumberOfIntersections = outputTrackList.Count * 2 + outputTrackList.Count; // worse case: 2 overlapping clips per track + each track |
|
m_CurrentListOfActiveClips = new List<RuntimeElement>(maximumNumberOfIntersections); |
|
m_ActiveClips = new List<RuntimeElement>(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<TrackAsset> 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>()) |
|
((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<PlayableDirector>(); |
|
playableOutput.SetUserData(director); |
|
foreach (var c in go.GetComponents<INotificationReceiver>()) |
|
{ |
|
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)); |
|
} |
|
|
|
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 = track.CreatePlayableGraph(graph, 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."); |
|
} |
|
|
|
|
|
// 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; |
|
} |
|
|
|
/// <summary> |
|
/// Overridden to handle synchronizing time on the timeline instance. |
|
/// </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 UNITY_EDITOR |
|
if (m_Rebalancer != null) |
|
m_Rebalancer.Rebalance(); |
|
// avoids loop creating a time offset during framelocked playback |
|
// if the timeline duration does not fall on a frame boundary. |
|
if (playableLooped != null && info.timeLooped) |
|
playableLooped.Invoke(playable); |
|
#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; |
|
} |
|
|
|
// 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 = (double)new DiscreteTime(playable.GetDuration()); |
|
foreach (var c in m_ActiveClips) |
|
{ |
|
if (c.intervalBit != m_ActiveBit) |
|
c.DisableAt(localTime, timelineEnd, frameData); |
|
} |
|
|
|
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<IntervalTree<RuntimeElement>.Entry>(); |
|
} |
|
} |
|
}
|
|
|