using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace UnityEngine.Timeline
{
///
/// A PlayableAsset representing a track inside a timeline.
///
[Serializable]
[IgnoreOnPlayableTrack]
public abstract partial class TrackAsset : PlayableAsset, IPropertyPreview, ICurvesOwner
{
// Internal caches used to avoid memory allocation during graph construction
private struct TransientBuildData
{
public List trackList;
public List clipList;
public List markerList;
public static TransientBuildData Create()
{
return new TransientBuildData()
{
trackList = new List(20),
clipList = new List(500),
markerList = new List(100),
};
}
public void Clear()
{
trackList.Clear();
clipList.Clear();
markerList.Clear();
}
}
private static TransientBuildData s_BuildData = TransientBuildData.Create();
internal const string kDefaultCurvesName = "Track Parameters";
internal static event Action OnClipPlayableCreate;
internal static event Action OnTrackAnimationPlayableCreate;
[SerializeField, HideInInspector] bool m_Locked;
[SerializeField, HideInInspector] bool m_Muted;
[SerializeField, HideInInspector] string m_CustomPlayableFullTypename = string.Empty;
[SerializeField, HideInInspector] AnimationClip m_Curves;
[SerializeField, HideInInspector] PlayableAsset m_Parent;
[SerializeField, HideInInspector] List m_Children;
[NonSerialized] int m_ItemsHash;
[NonSerialized] TimelineClip[] m_ClipsCache;
DiscreteTime m_Start;
DiscreteTime m_End;
bool m_CacheSorted;
bool? m_SupportsNotifications;
static TrackAsset[] s_EmptyCache = new TrackAsset[0];
IEnumerable m_ChildTrackCache;
static Dictionary s_TrackBindingTypeAttributeCache = new Dictionary();
[SerializeField, HideInInspector] protected internal List m_Clips = new List();
[SerializeField, HideInInspector] MarkerList m_Markers = new MarkerList(0);
#if UNITY_EDITOR
internal int DirtyIndex { get; private set; }
internal void MarkDirty()
{
DirtyIndex++;
foreach (var clip in GetClips())
{
if (clip != null)
clip.MarkDirty();
}
}
#endif
///
/// The start time, in seconds, of this track
///
public double start
{
get
{
UpdateDuration();
return (double)m_Start;
}
}
///
/// The end time, in seconds, of this track
///
public double end
{
get
{
UpdateDuration();
return (double)m_End;
}
}
///
/// The length, in seconds, of this track
///
public sealed override double duration
{
get
{
UpdateDuration();
return (double)(m_End - m_Start);
}
}
///
/// Whether the track is muted or not.
///
///
/// A muted track is excluded from the generated PlayableGraph
///
public bool muted
{
get { return m_Muted; }
set { m_Muted = value; }
}
///
/// The muted state of a track.
///
///
/// A track is also muted when one of its parent tracks are muted.
///
public bool mutedInHierarchy
{
get
{
if (muted)
return true;
TrackAsset p = this;
while (p.parent as TrackAsset != null)
{
p = (TrackAsset)p.parent;
if (p as GroupTrack != null)
return p.mutedInHierarchy;
}
return false;
}
}
///
/// The TimelineAsset that this track belongs to.
///
public TimelineAsset timelineAsset
{
get
{
var node = this;
while (node != null)
{
if (node.parent == null)
return null;
var seq = node.parent as TimelineAsset;
if (seq != null)
return seq;
node = node.parent as TrackAsset;
}
return null;
}
}
///
/// The owner of this track.
///
///
/// If this track is a subtrack, the parent is a TrackAsset. Otherwise the parent is a TimelineAsset.
///
public PlayableAsset parent
{
get { return m_Parent; }
internal set { m_Parent = value; }
}
///
/// A list of clips owned by this track
///
/// Returns an enumerable list of clips owned by the track.
public IEnumerable GetClips()
{
return clips;
}
internal TimelineClip[] clips
{
get
{
if (m_Clips == null)
m_Clips = new List();
if (m_ClipsCache == null)
{
m_CacheSorted = false;
m_ClipsCache = m_Clips.ToArray();
}
return m_ClipsCache;
}
}
///
/// Whether this track is considered empty.
///
///
/// A track is considered empty when it does not contain a TimelineClip, Marker, or Curve.
///
///
/// Empty tracks are not included in the playable graph.
///
public virtual bool isEmpty
{
get { return !hasClips && !hasCurves && GetMarkerCount() == 0; }
}
///
/// Whether this track contains any TimelineClip.
///
public bool hasClips
{
get { return m_Clips != null && m_Clips.Count != 0; }
}
///
/// Whether this track contains animated properties for the attached PlayableAsset.
///
///
/// This property is false if the curves property is null or if it contains no information.
///
public bool hasCurves
{
get { return m_Curves != null && !m_Curves.empty; }
}
///
/// Returns whether this track is a subtrack
///
public bool isSubTrack
{
get
{
var owner = parent as TrackAsset;
return owner != null && owner.GetType() == GetType();
}
}
///
/// Returns a description of the PlayableOutputs that will be created by this track.
///
public override IEnumerable outputs
{
get
{
TrackBindingTypeAttribute attribute;
if (!s_TrackBindingTypeAttributeCache.TryGetValue(GetType(), out attribute))
{
attribute = (TrackBindingTypeAttribute)Attribute.GetCustomAttribute(GetType(), typeof(TrackBindingTypeAttribute));
s_TrackBindingTypeAttributeCache.Add(GetType(), attribute);
}
var trackBindingType = attribute != null ? attribute.type : null;
yield return ScriptPlayableBinding.Create(name, this, trackBindingType);
}
}
///
/// The list of subtracks or child tracks attached to this track.
///
/// Returns an enumerable list of child tracks owned directly by this track.
///
/// In the case of GroupTracks, this returns all tracks contained in the group. This will return the all subtracks or override tracks, if supported by the track.
///
public IEnumerable GetChildTracks()
{
UpdateChildTrackCache();
return m_ChildTrackCache;
}
internal string customPlayableTypename
{
get { return m_CustomPlayableFullTypename; }
set { m_CustomPlayableFullTypename = value; }
}
///
/// An animation clip storing animated properties of the attached PlayableAsset
///
public AnimationClip curves
{
get { return m_Curves; }
internal set { m_Curves = value; }
}
string ICurvesOwner.defaultCurvesName
{
get { return kDefaultCurvesName; }
}
Object ICurvesOwner.asset
{
get { return this; }
}
Object ICurvesOwner.assetOwner
{
get { return timelineAsset; }
}
TrackAsset ICurvesOwner.targetTrack
{
get { return this; }
}
// for UI where we need to detect 'null' objects
internal List subTracksObjects
{
get { return m_Children; }
}
///
/// The local locked state of the track.
///
///
/// Note that locking a track only affects operations in the Timeline Editor. It does not prevent other API calls from changing a track or it's clips.
///
/// This returns or sets the local locked state of the track. A track may still be locked for editing because one or more of it's parent tracks in the hierarchy is locked. Use lockedInHierarchy to test if a track is locked because of it's own locked state or because of a parent tracks locked state.
///
public bool locked
{
get { return m_Locked; }
set { m_Locked = value; }
}
///
/// The locked state of a track. (RO)
///
///
/// Note that locking a track only affects operations in the Timeline Editor. It does not prevent other API calls from changing a track or it's clips.
///
/// This indicates whether a track is locked in the Timeline Editor because either it's locked property is enabled or a parent track is locked.
///
public bool lockedInHierarchy
{
get
{
if (locked)
return true;
TrackAsset p = this;
while (p.parent as TrackAsset != null)
{
p = (TrackAsset)p.parent;
if (p as GroupTrack != null)
return p.lockedInHierarchy;
}
return false;
}
}
///
/// Indicates if a track accepts markers that implement .
///
///
/// Only tracks with a bound object of type or can accept notifications.
///
public bool supportsNotifications
{
get
{
if (!m_SupportsNotifications.HasValue)
{
m_SupportsNotifications = NotificationUtilities.TrackTypeSupportsNotifications(GetType());
}
return m_SupportsNotifications.Value;
}
}
void __internalAwake() //do not use OnEnable, since users will want it to initialize their class
{
if (m_Clips == null)
m_Clips = new List();
m_ChildTrackCache = null;
if (m_Children == null)
m_Children = new List();
#if UNITY_EDITOR
// validate the array. DON'T remove Unity null objects, just actual null objects
for (int i = m_Children.Count - 1; i >= 0; i--)
{
object o = m_Children[i];
if (o == null)
{
Debug.LogWarning("Empty child track found while loading timeline. It will be removed.");
m_Children.RemoveAt(i);
}
}
#endif
}
///
/// Creates an AnimationClip to store animated properties for the attached PlayableAsset.
///
///
/// If curves already exists for this track, this method produces no result regardless of
/// the value specified for curvesClipName.
///
///
/// When used from the editor, this method attempts to save the created curves clip to the TimelineAsset.
/// The TimelineAsset must already exist in the AssetDatabase to save the curves clip. If the TimelineAsset
/// does not exist, the curves clip is still created but it is not saved.
///
///
/// The name of the AnimationClip to create.
/// This method does not ensure unique names. If you want a unique clip name, you must provide one.
/// See ObjectNames.GetUniqueName for information on a method that creates unique names.
///
public void CreateCurves(string curvesClipName)
{
if (m_Curves != null)
return;
m_Curves = TimelineCreateUtilities.CreateAnimationClipForTrack(string.IsNullOrEmpty(curvesClipName) ? kDefaultCurvesName : curvesClipName, this, true);
}
///
/// Creates a mixer used to blend playables generated by clips on the track.
///
/// The graph to inject playables into
/// The GameObject that requested the graph.
/// The number of playables from clips that will be inputs to the returned mixer
/// A handle to the [[Playable]] representing the mixer.
///
/// Override this method to provide a custom playable for mixing clips on a graph.
///
public virtual Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
return Playable.Create(graph, inputCount);
}
///
/// Overrides PlayableAsset.CreatePlayable(). Not used in Timeline.
///
public sealed override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
return Playable.Null;
}
///
/// Creates a TimelineClip on this track.
///
/// Returns a new TimelineClip that is attached to the track.
///
/// The type of the playable asset attached to the clip is determined by TrackClip attributes that decorate the TrackAsset derived class
///
public TimelineClip CreateDefaultClip()
{
var trackClipTypeAttributes = GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
Type playableAssetType = null;
foreach (var trackClipTypeAttribute in trackClipTypeAttributes)
{
var attribute = trackClipTypeAttribute as TrackClipTypeAttribute;
if (attribute != null && typeof(IPlayableAsset).IsAssignableFrom(attribute.inspectedType) && typeof(ScriptableObject).IsAssignableFrom(attribute.inspectedType))
{
playableAssetType = attribute.inspectedType;
break;
}
}
if (playableAssetType == null)
{
Debug.LogWarning("Cannot create a default clip for type " + GetType());
return null;
}
return CreateAndAddNewClipOfType(playableAssetType);
}
///
/// Creates a clip on the track with a playable asset attached, whose derived type is specified by T
///
/// A PlayableAsset derived type
/// Returns a TimelineClip whose asset is of type T
///
/// Throws an InvalidOperationException if the specified type is not supported by the track.
/// Supported types are determined by TrackClip attributes that decorate the TrackAsset derived class
///
public TimelineClip CreateClip() where T : ScriptableObject, IPlayableAsset
{
return CreateClip(typeof(T));
}
///
/// Creates a marker of the requested type, at a specific time, and adds the marker to the current asset.
///
/// The type of marker.
/// The time where the marker is created.
/// Returns the instance of the created marker.
///
/// All markers that implement IMarker and inherit from are supported.
/// Markers that implement the INotification interface cannot be added to tracks that do not support notifications.
/// CreateMarker will throw an InvalidOperationException
with tracks that do not support notifications if type
implements the INotification interface.
///
///
///
public IMarker CreateMarker(Type type, double time)
{
return m_Markers.CreateMarker(type, time, this);
}
///
/// Creates a marker of the requested type, at a specific time, and adds the marker to the current asset.
///
/// The time where the marker is created.
/// Returns the instance of the created marker.
///
/// All markers that implement IMarker and inherit from are supported.
/// CreateMarker will throw an InvalidOperationException
with tracks that do not support notifications if T
implements the INotification interface.
///
///
///
public T CreateMarker(double time) where T : ScriptableObject, IMarker
{
return (T)CreateMarker(typeof(T), time);
}
///
/// Removes a marker from the current asset.
///
/// The marker instance to be removed.
/// Returns true if the marker instance was successfully removed. Returns false otherwise.
public bool DeleteMarker(IMarker marker)
{
return m_Markers.Remove(marker);
}
///
/// Returns an enumerable list of markers on the current asset.
///
/// The list of markers on the asset.
///
public IEnumerable GetMarkers()
{
return m_Markers.GetMarkers();
}
///
/// Returns the number of markers on the current asset.
///
/// The number of markers.
public int GetMarkerCount()
{
return m_Markers.Count;
}
///
/// Returns the marker at a given position, on the current asset.
///
/// The index of the marker to be returned.
/// The marker.
/// The ordering of the markers is not guaranteed.
///
public IMarker GetMarker(int idx)
{
return m_Markers[idx];
}
internal TimelineClip CreateClip(System.Type requestedType)
{
if (ValidateClipType(requestedType))
return CreateAndAddNewClipOfType(requestedType);
throw new InvalidOperationException("Clips of type " + requestedType + " are not permitted on tracks of type " + GetType());
}
internal TimelineClip CreateAndAddNewClipOfType(Type requestedType)
{
var newClip = CreateClipOfType(requestedType);
AddClip(newClip);
return newClip;
}
internal TimelineClip CreateClipOfType(Type requestedType)
{
if (!ValidateClipType(requestedType))
throw new System.InvalidOperationException("Clips of type " + requestedType + " are not permitted on tracks of type " + GetType());
var playableAsset = CreateInstance(requestedType);
if (playableAsset == null)
{
throw new System.InvalidOperationException("Could not create an instance of the ScriptableObject type " + requestedType.Name);
}
playableAsset.name = requestedType.Name;
TimelineCreateUtilities.SaveAssetIntoObject(playableAsset, this);
TimelineUndo.RegisterCreatedObjectUndo(playableAsset, "Create Clip");
return CreateClipFromAsset(playableAsset);
}
///
/// Creates a timeline clip from an existing playable asset.
///
///
///
internal TimelineClip CreateClipFromPlayableAsset(IPlayableAsset asset)
{
if (asset == null)
throw new ArgumentNullException("asset");
if ((asset as ScriptableObject) == null)
throw new System.ArgumentException("CreateClipFromPlayableAsset " + " only supports ScriptableObject-derived Types");
if (!ValidateClipType(asset.GetType()))
throw new System.InvalidOperationException("Clips of type " + asset.GetType() + " are not permitted on tracks of type " + GetType());
return CreateClipFromAsset(asset as ScriptableObject);
}
private TimelineClip CreateClipFromAsset(ScriptableObject playableAsset)
{
TimelineUndo.PushUndo(this, "Create Clip");
var newClip = CreateNewClipContainerInternal();
newClip.displayName = playableAsset.name;
newClip.asset = playableAsset;
IPlayableAsset iPlayableAsset = playableAsset as IPlayableAsset;
if (iPlayableAsset != null)
{
var candidateDuration = iPlayableAsset.duration;
if (!double.IsInfinity(candidateDuration) && candidateDuration > 0)
newClip.duration = Math.Min(Math.Max(candidateDuration, TimelineClip.kMinDuration), TimelineClip.kMaxTimeValue);
}
try
{
OnCreateClip(newClip);
}
catch (Exception e)
{
Debug.LogError(e.Message, playableAsset);
return null;
}
return newClip;
}
internal IEnumerable GetMarkersRaw()
{
return m_Markers.GetRawMarkerList();
}
internal void ClearMarkers()
{
m_Markers.Clear();
}
internal void AddMarker(ScriptableObject e)
{
m_Markers.Add(e);
}
internal bool DeleteMarkerRaw(ScriptableObject marker)
{
return m_Markers.Remove(marker, timelineAsset, this);
}
int GetTimeRangeHash()
{
double start = double.MaxValue, end = double.MinValue;
foreach (var marker in GetMarkers())
{
if (!(marker is INotification))
{
continue;
}
if (marker.time < start)
start = marker.time;
if (marker.time > end)
end = marker.time;
}
return start.GetHashCode().CombineHash(end.GetHashCode());
}
internal void AddClip(TimelineClip newClip)
{
if (!m_Clips.Contains(newClip))
{
m_Clips.Add(newClip);
m_ClipsCache = null;
}
}
Playable CreateNotificationsPlayable(PlayableGraph graph, Playable mixerPlayable, GameObject go, Playable timelinePlayable)
{
s_BuildData.markerList.Clear();
GatherNotificiations(s_BuildData.markerList);
var notificationPlayable = NotificationUtilities.CreateNotificationsPlayable(graph, s_BuildData.markerList, go);
if (notificationPlayable.IsValid())
{
notificationPlayable.GetBehaviour().timeSource = timelinePlayable;
if (mixerPlayable.IsValid())
{
notificationPlayable.SetInputCount(1);
graph.Connect(mixerPlayable, 0, notificationPlayable, 0);
notificationPlayable.SetInputWeight(mixerPlayable, 1);
}
}
return notificationPlayable;
}
internal Playable CreatePlayableGraph(PlayableGraph graph, GameObject go, IntervalTree tree, Playable timelinePlayable)
{
UpdateDuration();
var mixerPlayable = Playable.Null;
if (CanCompileClipsRecursive())
mixerPlayable = OnCreateClipPlayableGraph(graph, go, tree);
var notificationsPlayable = CreateNotificationsPlayable(graph, mixerPlayable, go, timelinePlayable);
// clear the temporary build data to avoid holding references
// case 1253974
s_BuildData.Clear();
if (!notificationsPlayable.IsValid() && !mixerPlayable.IsValid())
{
Debug.LogErrorFormat("Track {0} of type {1} has no notifications and returns an invalid mixer Playable", name,
GetType().FullName);
return Playable.Create(graph);
}
return notificationsPlayable.IsValid() ? notificationsPlayable : mixerPlayable;
}
internal virtual Playable CompileClips(PlayableGraph graph, GameObject go, IList timelineClips, IntervalTree tree)
{
var blend = CreateTrackMixer(graph, go, timelineClips.Count);
for (var c = 0; c < timelineClips.Count; c++)
{
var source = CreatePlayable(graph, go, timelineClips[c]);
if (source.IsValid())
{
source.SetDuration(timelineClips[c].duration);
var clip = new RuntimeClip(timelineClips[c], source, blend);
tree.Add(clip);
graph.Connect(source, 0, blend, c);
blend.SetInputWeight(c, 0.0f);
}
}
ConfigureTrackAnimation(tree, go, blend);
return blend;
}
void GatherCompilableTracks(IList tracks)
{
if (!muted && CanCompileClips())
tracks.Add(this);
foreach (var c in GetChildTracks())
{
if (c != null)
c.GatherCompilableTracks(tracks);
}
}
void GatherNotificiations(List markers)
{
if (!muted && CanCompileNotifications())
markers.AddRange(GetMarkers());
foreach (var c in GetChildTracks())
{
if (c != null)
c.GatherNotificiations(markers);
}
}
internal virtual Playable OnCreateClipPlayableGraph(PlayableGraph graph, GameObject go, IntervalTree tree)
{
if (tree == null)
throw new ArgumentException("IntervalTree argument cannot be null", "tree");
if (go == null)
throw new ArgumentException("GameObject argument cannot be null", "go");
s_BuildData.Clear();
GatherCompilableTracks(s_BuildData.trackList);
// nothing to compile
if (s_BuildData.trackList.Count == 0)
return Playable.Null;
// check if layers are supported
Playable layerMixer = Playable.Null;
ILayerable layerable = this as ILayerable;
if (layerable != null)
layerMixer = layerable.CreateLayerMixer(graph, go, s_BuildData.trackList.Count);
if (layerMixer.IsValid())
{
for (int i = 0; i < s_BuildData.trackList.Count; i++)
{
var mixer = s_BuildData.trackList[i].CompileClips(graph, go, s_BuildData.trackList[i].clips, tree);
if (mixer.IsValid())
{
graph.Connect(mixer, 0, layerMixer, i);
layerMixer.SetInputWeight(i, 1.0f);
}
}
return layerMixer;
}
// one track compiles. Add track mixer and clips
if (s_BuildData.trackList.Count == 1)
return s_BuildData.trackList[0].CompileClips(graph, go, s_BuildData.trackList[0].clips, tree);
// no layer mixer provided. merge down all clips.
for (int i = 0; i < s_BuildData.trackList.Count; i++)
s_BuildData.clipList.AddRange(s_BuildData.trackList[i].clips);
#if UNITY_EDITOR
bool applyWarning = false;
for (int i = 0; i < s_BuildData.trackList.Count; i++)
applyWarning |= i > 0 && s_BuildData.trackList[i].hasCurves;
if (applyWarning)
Debug.LogWarning("A layered track contains animated fields, but no layer mixer has been provided. Animated fields on layers will be ignored. Override CreateLayerMixer in " + s_BuildData.trackList[0].GetType().Name + " and return a valid playable to support animated fields on layered tracks.");
#endif
// compile all the clips into a single mixer
return CompileClips(graph, go, s_BuildData.clipList, tree);
}
internal void ConfigureTrackAnimation(IntervalTree tree, GameObject go, Playable blend)
{
if (!hasCurves)
return;
blend.SetAnimatedProperties(m_Curves);
tree.Add(new InfiniteRuntimeClip(blend));
if (OnTrackAnimationPlayableCreate != null)
OnTrackAnimationPlayableCreate.Invoke(this, go, blend);
}
// sorts clips by start time
internal void SortClips()
{
var clipsAsArray = clips; // will alloc
if (!m_CacheSorted)
{
Array.Sort(clips, (clip1, clip2) => clip1.start.CompareTo(clip2.start));
m_CacheSorted = true;
}
}
// clears the clips after a clone
internal void ClearClipsInternal()
{
m_Clips = new List();
m_ClipsCache = null;
}
internal void ClearSubTracksInternal()
{
m_Children = new List();
Invalidate();
}
// called by an owned clip when it moves
internal void OnClipMove()
{
m_CacheSorted = false;
}
internal TimelineClip CreateNewClipContainerInternal()
{
var clipContainer = new TimelineClip(this);
clipContainer.asset = null;
// position clip at end of sequence
var newClipStart = 0.0;
for (var a = 0; a < m_Clips.Count - 1; a++)
{
var clipDuration = m_Clips[a].duration;
if (double.IsInfinity(clipDuration))
clipDuration = TimelineClip.kDefaultClipDurationInSeconds;
newClipStart = Math.Max(newClipStart, m_Clips[a].start + clipDuration);
}
clipContainer.mixInCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
clipContainer.mixOutCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
clipContainer.start = newClipStart;
clipContainer.duration = TimelineClip.kDefaultClipDurationInSeconds;
clipContainer.displayName = "untitled";
return clipContainer;
}
internal void AddChild(TrackAsset child)
{
if (child == null)
return;
m_Children.Add(child);
child.parent = this;
Invalidate();
}
internal void MoveLastTrackBefore(TrackAsset asset)
{
if (m_Children == null || m_Children.Count < 2 || asset == null)
return;
var lastTrack = m_Children[m_Children.Count - 1];
if (lastTrack == asset)
return;
for (int i = 0; i < m_Children.Count - 1; i++)
{
if (m_Children[i] == asset)
{
for (int j = m_Children.Count - 1; j > i; j--)
m_Children[j] = m_Children[j - 1];
m_Children[i] = lastTrack;
Invalidate();
break;
}
}
}
internal bool RemoveSubTrack(TrackAsset child)
{
if (m_Children.Remove(child))
{
Invalidate();
child.parent = null;
return true;
}
return false;
}
internal void RemoveClip(TimelineClip clip)
{
m_Clips.Remove(clip);
m_ClipsCache = null;
}
// Is this track compilable for the sequence
// calculate the time interval that this track will be evaluated in.
internal virtual void GetEvaluationTime(out double outStart, out double outDuration)
{
outStart = double.PositiveInfinity;
var outEnd = double.NegativeInfinity;
if (hasCurves)
{
outStart = 0.0;
outEnd = TimeUtility.GetAnimationClipLength(curves);
}
foreach (var clip in clips)
{
outStart = Math.Min(clip.start, outStart);
outEnd = Math.Max(clip.end, outEnd);
}
if (HasNotifications())
{
var notificationDuration = GetNotificationDuration();
outStart = Math.Min(notificationDuration, outStart);
outEnd = Math.Max(notificationDuration, outEnd);
}
if (double.IsInfinity(outStart) || double.IsInfinity(outEnd))
outStart = outDuration = 0.0;
else
outDuration = outEnd - outStart;
}
// calculate the time interval that the sequence will use to determine length.
// by default this is the same as the evaluation, but subclasses can have different
// behaviour
internal virtual void GetSequenceTime(out double outStart, out double outDuration)
{
GetEvaluationTime(out outStart, out outDuration);
}
///
/// Called by the Timeline Editor to gather properties requiring preview.
///
/// The PlayableDirector invoking the preview
/// PropertyCollector used to gather previewable properties
public virtual void GatherProperties(PlayableDirector director, IPropertyCollector driver)
{
// only push on game objects if there is a binding. Subtracks
// will use objects on the stack
var gameObject = GetGameObjectBinding(director);
if (gameObject != null)
driver.PushActiveGameObject(gameObject);
if (hasCurves)
driver.AddObjectProperties(this, m_Curves);
foreach (var clip in clips)
{
if (clip.curves != null && clip.asset != null)
driver.AddObjectProperties(clip.asset, clip.curves);
IPropertyPreview modifier = clip.asset as IPropertyPreview;
if (modifier != null)
modifier.GatherProperties(director, driver);
}
foreach (var subtrack in GetChildTracks())
{
if (subtrack != null)
subtrack.GatherProperties(director, driver);
}
if (gameObject != null)
driver.PopActiveGameObject();
}
internal GameObject GetGameObjectBinding(PlayableDirector director)
{
if (director == null)
return null;
var binding = director.GetGenericBinding(this);
var gameObject = binding as GameObject;
if (gameObject != null)
return gameObject;
var comp = binding as Component;
if (comp != null)
return comp.gameObject;
return null;
}
internal bool ValidateClipType(Type clipType)
{
var attrs = GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
for (var c = 0; c < attrs.Length; ++c)
{
var attr = (TrackClipTypeAttribute)attrs[c];
if (attr.inspectedType.IsAssignableFrom(clipType))
return true;
}
// special case for playable tracks, they accept all clips (in the runtime)
return typeof(PlayableTrack).IsAssignableFrom(GetType()) &&
typeof(IPlayableAsset).IsAssignableFrom(clipType) &&
typeof(ScriptableObject).IsAssignableFrom(clipType);
}
///
/// Called when a clip is created on a track.
///
/// The timeline clip added to this track
/// Use this method to set default values on a timeline clip, or it's PlayableAsset.
protected virtual void OnCreateClip(TimelineClip clip) {}
void UpdateDuration()
{
// check if something changed in the clips that require a re-calculation of the evaluation times.
var itemsHash = CalculateItemsHash();
if (itemsHash == m_ItemsHash)
return;
m_ItemsHash = itemsHash;
double trackStart, trackDuration;
GetSequenceTime(out trackStart, out trackDuration);
m_Start = (DiscreteTime)trackStart;
m_End = (DiscreteTime)(trackStart + trackDuration);
// calculate the extrapolations time.
// TODO Extrapolation time should probably be extracted from the SequenceClip so only a track is aware of it.
this.CalculateExtrapolationTimes();
}
protected internal virtual int CalculateItemsHash()
{
return HashUtility.CombineHash(GetClipsHash(), GetAnimationClipHash(m_Curves), GetTimeRangeHash());
}
///
/// Constructs a Playable from a TimelineClip.
///
/// PlayableGraph that will own the playable.
/// The GameObject that builds the PlayableGraph.
/// The TimelineClip to construct a playable for.
/// A playable that will be set as an input to the Track Mixer playable, or Playable.Null if the clip does not have a valid PlayableAsset
/// Thrown if the specified PlayableGraph is not valid.
/// Thrown if the specified TimelineClip is not valid.
///
/// By default, this method invokes Playable.CreatePlayable, sets animated properties, and sets the speed of the created playable. Override this method to change this default implementation.
///
protected virtual Playable CreatePlayable(PlayableGraph graph, GameObject gameObject, TimelineClip clip)
{
if (!graph.IsValid())
throw new ArgumentException("graph must be a valid PlayableGraph");
if (clip == null)
throw new ArgumentNullException("clip");
var asset = clip.asset as IPlayableAsset;
if (asset != null)
{
var handle = asset.CreatePlayable(graph, gameObject);
if (handle.IsValid())
{
handle.SetAnimatedProperties(clip.curves);
handle.SetSpeed(clip.timeScale);
if (OnClipPlayableCreate != null)
OnClipPlayableCreate(clip, gameObject, handle);
}
return handle;
}
return Playable.Null;
}
internal void Invalidate()
{
m_ChildTrackCache = null;
var timeline = timelineAsset;
if (timeline != null)
{
timeline.Invalidate();
}
}
internal double GetNotificationDuration()
{
if (!supportsNotifications)
{
return 0;
}
var maxTime = 0.0;
foreach (var marker in GetMarkers())
{
if (!(marker is INotification))
{
continue;
}
maxTime = Math.Max(maxTime, marker.time);
}
return maxTime;
}
internal virtual bool CanCompileClips()
{
return hasClips || hasCurves;
}
internal bool IsCompilable()
{
var isContainer = typeof(GroupTrack).IsAssignableFrom(GetType());
if (isContainer)
return false;
var ret = !mutedInHierarchy && (CanCompileClips() || CanCompileNotifications());
if (!ret)
{
foreach (var t in GetChildTracks())
{
if (t.IsCompilable())
return true;
}
}
return ret;
}
private void UpdateChildTrackCache()
{
if (m_ChildTrackCache == null)
{
if (m_Children == null || m_Children.Count == 0)
m_ChildTrackCache = s_EmptyCache;
else
{
var childTracks = new List(m_Children.Count);
for (int i = 0; i < m_Children.Count; i++)
{
var subTrack = m_Children[i] as TrackAsset;
if (subTrack != null)
childTracks.Add(subTrack);
}
m_ChildTrackCache = childTracks;
}
}
}
internal virtual int Hash()
{
return clips.Length + (m_Markers.Count << 16);
}
int GetClipsHash()
{
var hash = 0;
foreach (var clip in m_Clips)
{
hash = hash.CombineHash(clip.Hash());
}
return hash;
}
protected static int GetAnimationClipHash(AnimationClip clip)
{
var hash = 0;
if (clip != null && !clip.empty)
hash = hash.CombineHash(clip.frameRate.GetHashCode())
.CombineHash(clip.length.GetHashCode());
return hash;
}
bool HasNotifications()
{
return m_Markers.HasNotifications();
}
bool CanCompileNotifications()
{
return supportsNotifications && m_Markers.HasNotifications();
}
bool CanCompileClipsRecursive()
{
if (CanCompileClips())
return true;
foreach (var track in GetChildTracks())
{
if (track.CanCompileClipsRecursive())
return true;
}
return false;
}
}
}