using System; using System.Collections.Generic; using UnityEngine.Animations; using UnityEngine.Playables; namespace UnityEngine.Timeline { /// /// A PlayableAsset representing a track inside a timeline. /// /// /// /// Derive from TrackAsset to implement custom timeline tracks. TrackAsset derived classes support the following attributes: /// /// /// /// /// /// /// /// /// /// [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. /// 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 if 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)); } /// /// Delete a clip from this track. /// /// The clip to delete. /// Returns true if the removal was successful /// /// This method will delete a clip and any assets owned by the clip. /// /// /// Throws if is not a child of the TrackAsset. /// public bool DeleteClip(TimelineClip clip) { if (!m_Clips.Contains(clip)) throw new InvalidOperationException("Cannot delete clip since it is not a child of the TrackAsset."); return timelineAsset != null && timelineAsset.DeleteClip(clip); } /// /// 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 with tracks that do not support notifications if 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. /// The type of marker to create. /// Returns the instance of the created marker. /// /// All markers that implement IMarker and inherit from are supported. /// CreateMarker will throw with tracks that do not support notifications if 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; int count = m_Markers.Count; for (int i = 0; i < m_Markers.Count; i++) { var marker = m_Markers[i]; 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(); GatherNotifications(s_BuildData.markerList); DirectorWrapMode extrapolationMode = DirectorWrapMode.None; if (go.TryGetComponent(out PlayableDirector director)) { extrapolationMode = director.extrapolationMode; } var duration = timelineAsset.duration; var notificationPlayable = NotificationUtilities.CreateNotificationsPlayable(graph, s_BuildData.markerList, duration, extrapolationMode); 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 (CanCreateMixerRecursive()) mixerPlayable = CreateMixerPlayableGraph(graph, go, tree); Playable 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 && CanCreateTrackMixer()) tracks.Add(this); foreach (var c in GetChildTracks()) { if (c != null) c.GatherCompilableTracks(tracks); } } void GatherNotifications(List markers) { if (!muted && CanCompileNotifications()) markers.AddRange(GetMarkers()); foreach (var c in GetChildTracks()) { if (c != null) c.GatherNotifications(markers); } } internal virtual Playable CreateMixerPlayableGraph(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 = 0; outDuration = 1; 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; int count = m_Markers.Count; for (int i = 0; i < count; i++) { var marker = m_Markers[i]; if (!(marker is INotification)) { continue; } maxTime = Math.Max(maxTime, marker.time); } return maxTime; } internal virtual bool CanCompileClips() { return hasClips || hasCurves; } /// /// Whether the track can create a mixer for its own contents. /// /// Returns true if the track's mixer should be included in the playable graph. /// A return value of true does not guarantee that the mixer will be included in the playable graph. GroupTracks and muted tracks are never included in the graph /// A return value of false does not guarantee that the mixer will not be included in the playable graph. If a child track returns true for CanCreateTrackMixer, the parent track will generate the mixer but its own playables will not be included. /// Override this method to change the conditions for a track to be included in the playable graph. public virtual bool CanCreateTrackMixer() { return CanCompileClips(); } internal bool IsCompilable() { bool isContainer = typeof(GroupTrack).IsAssignableFrom(GetType()); if (isContainer) return false; var ret = !mutedInHierarchy && (CanCreateTrackMixer() || 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; } /// /// Gets the hash code for an AnimationClip. /// /// The animation clip. /// A 32-bit signed integer that is the hash code for . Returns 0 if is null or empty. 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 CanCreateMixerRecursive() { if (CanCreateTrackMixer()) return true; foreach (var track in GetChildTracks()) { if (track.CanCreateMixerRecursive()) return true; } return false; } } }