A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEngine.Animations;
4using UnityEngine.Playables;
5
6namespace UnityEngine.Timeline
7{
8 /// <summary>
9 /// A PlayableAsset representing a track inside a timeline.
10 /// </summary>
11 ///
12 /// <remarks>
13 /// Derive from TrackAsset to implement custom timeline tracks. TrackAsset derived classes support the following attributes:
14 /// <seealso cref="UnityEngine.Timeline.HideInMenuAttribute"/>
15 /// <seealso cref="UnityEngine.Timeline.TrackColorAttribute"/>
16 /// <seealso cref="UnityEngine.Timeline.TrackClipTypeAttribute"/>
17 /// <seealso cref="UnityEngine.Timeline.TrackBindingTypeAttribute"/>
18 /// <seealso cref="System.ComponentModel.DisplayNameAttribute"/>
19 /// </remarks>
20 ///
21 /// <example>
22 /// <code source="../../DocCodeExamples/TrackAssetExamples.cs" region="declare-trackAssetExample" title="TrackAssetExample"/>
23 /// </example>
24 [Serializable]
25 [IgnoreOnPlayableTrack]
26 public abstract partial class TrackAsset : PlayableAsset, IPropertyPreview, ICurvesOwner
27 {
28 // Internal caches used to avoid memory allocation during graph construction
29 private struct TransientBuildData
30 {
31 public List<TrackAsset> trackList;
32 public List<TimelineClip> clipList;
33 public List<IMarker> markerList;
34
35 public static TransientBuildData Create()
36 {
37 return new TransientBuildData()
38 {
39 trackList = new List<TrackAsset>(20),
40 clipList = new List<TimelineClip>(500),
41 markerList = new List<IMarker>(100),
42 };
43 }
44
45 public void Clear()
46 {
47 trackList.Clear();
48 clipList.Clear();
49 markerList.Clear();
50 }
51 }
52
53 private static TransientBuildData s_BuildData = TransientBuildData.Create();
54
55 internal const string kDefaultCurvesName = "Track Parameters";
56
57 internal static event Action<TimelineClip, GameObject, Playable> OnClipPlayableCreate;
58 internal static event Action<TrackAsset, GameObject, Playable> OnTrackAnimationPlayableCreate;
59
60 [SerializeField, HideInInspector] bool m_Locked;
61 [SerializeField, HideInInspector] bool m_Muted;
62 [SerializeField, HideInInspector] string m_CustomPlayableFullTypename = string.Empty;
63 [SerializeField, HideInInspector] AnimationClip m_Curves;
64 [SerializeField, HideInInspector] PlayableAsset m_Parent;
65 [SerializeField, HideInInspector] List<ScriptableObject> m_Children;
66
67 [NonSerialized] int m_ItemsHash;
68 [NonSerialized] TimelineClip[] m_ClipsCache;
69
70 DiscreteTime m_Start;
71 DiscreteTime m_End;
72 bool m_CacheSorted;
73 bool? m_SupportsNotifications;
74
75 static TrackAsset[] s_EmptyCache = new TrackAsset[0];
76 IEnumerable<TrackAsset> m_ChildTrackCache;
77
78 static Dictionary<Type, TrackBindingTypeAttribute> s_TrackBindingTypeAttributeCache = new Dictionary<Type, TrackBindingTypeAttribute>();
79
80 [SerializeField, HideInInspector] protected internal List<TimelineClip> m_Clips = new List<TimelineClip>();
81
82 [SerializeField, HideInInspector] MarkerList m_Markers = new MarkerList(0);
83
84#if UNITY_EDITOR
85 internal int DirtyIndex { get; private set; }
86 internal void MarkDirtyTrackAndClips()
87 {
88 DirtyIndex++;
89 foreach (var clip in GetClips())
90 {
91 if (clip != null)
92 clip.MarkDirty();
93 }
94 }
95#endif
96
97 /// <summary>
98 /// The start time, in seconds, of this track
99 /// </summary>
100 public double start
101 {
102 get
103 {
104 UpdateDuration();
105 return (double)m_Start;
106 }
107 }
108
109 /// <summary>
110 /// The end time, in seconds, of this track
111 /// </summary>
112 public double end
113 {
114 get
115 {
116 UpdateDuration();
117 return (double)m_End;
118 }
119 }
120
121 /// <summary>
122 /// The length, in seconds, of this track
123 /// </summary>
124 public sealed override double duration
125 {
126 get
127 {
128 UpdateDuration();
129 return (double)(m_End - m_Start);
130 }
131 }
132
133 /// <summary>
134 /// Whether the track is muted or not.
135 /// </summary>
136 /// <remarks>
137 /// A muted track is excluded from the generated PlayableGraph
138 /// </remarks>
139 public bool muted
140 {
141 get { return m_Muted; }
142 set { m_Muted = value; }
143 }
144
145 /// <summary>
146 /// The muted state of a track.
147 /// </summary>
148 /// <remarks>
149 /// A track is also muted when one of its parent tracks are muted.
150 /// </remarks>
151 public bool mutedInHierarchy
152 {
153 get
154 {
155 if (muted)
156 return true;
157
158 TrackAsset p = this;
159 while (p.parent as TrackAsset != null)
160 {
161 p = (TrackAsset)p.parent;
162 if (p as GroupTrack != null)
163 return p.mutedInHierarchy;
164 }
165
166 return false;
167 }
168 }
169
170 /// <summary>
171 /// The TimelineAsset that this track belongs to.
172 /// </summary>
173 public TimelineAsset timelineAsset
174 {
175 get
176 {
177 var node = this;
178 while (node != null)
179 {
180 if (node.parent == null)
181 return null;
182
183 var seq = node.parent as TimelineAsset;
184 if (seq != null)
185 return seq;
186
187 node = node.parent as TrackAsset;
188 }
189 return null;
190 }
191 }
192
193 /// <summary>
194 /// The owner of this track.
195 /// </summary>
196 /// <remarks>
197 /// If this track is a subtrack, the parent is a TrackAsset. Otherwise the parent is a TimelineAsset.
198 /// </remarks>
199 public PlayableAsset parent
200 {
201 get { return m_Parent; }
202 internal set { m_Parent = value; }
203 }
204
205 /// <summary>
206 /// A list of clips owned by this track
207 /// </summary>
208 /// <returns>Returns an enumerable list of clips owned by the track.</returns>
209 public IEnumerable<TimelineClip> GetClips()
210 {
211 return clips;
212 }
213
214 internal TimelineClip[] clips
215 {
216 get
217 {
218 if (m_Clips == null)
219 m_Clips = new List<TimelineClip>();
220
221 if (m_ClipsCache == null)
222 {
223 m_CacheSorted = false;
224 m_ClipsCache = m_Clips.ToArray();
225 }
226
227 return m_ClipsCache;
228 }
229 }
230
231 /// <summary>
232 /// Whether this track is considered empty.
233 /// </summary>
234 /// <remarks>
235 /// A track is considered empty when it does not contain a TimelineClip, Marker, or Curve.
236 /// </remarks>
237 public virtual bool isEmpty
238 {
239 get { return !hasClips && !hasCurves && GetMarkerCount() == 0; }
240 }
241
242 /// <summary>
243 /// Whether this track contains any TimelineClip.
244 /// </summary>
245 public bool hasClips
246 {
247 get { return m_Clips != null && m_Clips.Count != 0; }
248 }
249
250 /// <summary>
251 /// Whether this track contains animated properties for the attached PlayableAsset.
252 /// </summary>
253 /// <remarks>
254 /// This property is false if the curves property is null or if it contains no information.
255 /// </remarks>
256 public bool hasCurves
257 {
258 get { return m_Curves != null && !m_Curves.empty; }
259 }
260
261 /// <summary>
262 /// Returns whether this track is a subtrack
263 /// </summary>
264 public bool isSubTrack
265 {
266 get
267 {
268 var owner = parent as TrackAsset;
269 return owner != null && owner.GetType() == GetType();
270 }
271 }
272
273
274 /// <summary>
275 /// Returns a description of the PlayableOutputs that will be created by this track.
276 /// </summary>
277 public override IEnumerable<PlayableBinding> outputs
278 {
279 get
280 {
281 TrackBindingTypeAttribute attribute;
282 if (!s_TrackBindingTypeAttributeCache.TryGetValue(GetType(), out attribute))
283 {
284 attribute = (TrackBindingTypeAttribute)Attribute.GetCustomAttribute(GetType(), typeof(TrackBindingTypeAttribute));
285 s_TrackBindingTypeAttributeCache.Add(GetType(), attribute);
286 }
287
288 var trackBindingType = attribute != null ? attribute.type : null;
289 yield return ScriptPlayableBinding.Create(name, this, trackBindingType);
290 }
291 }
292
293 /// <summary>
294 /// The list of subtracks or child tracks attached to this track.
295 /// </summary>
296 /// <returns>Returns an enumerable list of child tracks owned directly by this track.</returns>
297 /// <remarks>
298 /// 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.
299 /// </remarks>
300 public IEnumerable<TrackAsset> GetChildTracks()
301 {
302 UpdateChildTrackCache();
303 return m_ChildTrackCache;
304 }
305
306 internal string customPlayableTypename
307 {
308 get { return m_CustomPlayableFullTypename; }
309 set { m_CustomPlayableFullTypename = value; }
310 }
311
312 /// <summary>
313 /// An animation clip storing animated properties of the attached PlayableAsset
314 /// </summary>
315 public AnimationClip curves
316 {
317 get { return m_Curves; }
318 internal set { m_Curves = value; }
319 }
320
321 string ICurvesOwner.defaultCurvesName
322 {
323 get { return kDefaultCurvesName; }
324 }
325
326 Object ICurvesOwner.asset
327 {
328 get { return this; }
329 }
330
331 Object ICurvesOwner.assetOwner
332 {
333 get { return timelineAsset; }
334 }
335
336 TrackAsset ICurvesOwner.targetTrack
337 {
338 get { return this; }
339 }
340
341 // for UI where we need to detect 'null' objects
342 internal List<ScriptableObject> subTracksObjects
343 {
344 get { return m_Children; }
345 }
346
347 /// <summary>
348 /// The local locked state of the track.
349 /// </summary>
350 /// <remarks>
351 /// 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.
352 ///
353 /// 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.
354 /// </remarks>
355 public bool locked
356 {
357 get { return m_Locked; }
358 set { m_Locked = value; }
359 }
360
361 /// <summary>
362 /// The locked state of a track. (RO)
363 /// </summary>
364 /// <remarks>
365 /// 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.
366 ///
367 /// 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.
368 /// </remarks>
369 public bool lockedInHierarchy
370 {
371 get
372 {
373 if (locked)
374 return true;
375
376 TrackAsset p = this;
377 while (p.parent as TrackAsset != null)
378 {
379 p = (TrackAsset)p.parent;
380 if (p as GroupTrack != null)
381 return p.lockedInHierarchy;
382 }
383
384 return false;
385 }
386 }
387
388 /// <summary>
389 /// Indicates if a track accepts markers that implement <see cref="UnityEngine.Playables.INotification"/>.
390 /// </summary>
391 /// <remarks>
392 /// Only tracks with a bound object of type <see cref="UnityEngine.GameObject"/> or <see cref="UnityEngine.Component"/> can accept notifications.
393 /// </remarks>
394 public bool supportsNotifications
395 {
396 get
397 {
398 if (!m_SupportsNotifications.HasValue)
399 {
400 m_SupportsNotifications = NotificationUtilities.TrackTypeSupportsNotifications(GetType());
401 }
402
403 return m_SupportsNotifications.Value;
404 }
405 }
406
407 void __internalAwake() //do not use OnEnable, since users will want it to initialize their class
408 {
409 if (m_Clips == null)
410 m_Clips = new List<TimelineClip>();
411
412 m_ChildTrackCache = null;
413 if (m_Children == null)
414 m_Children = new List<ScriptableObject>();
415#if UNITY_EDITOR
416 // validate the array. DON'T remove Unity null objects, just actual null objects
417 for (int i = m_Children.Count - 1; i >= 0; i--)
418 {
419 object o = m_Children[i];
420 if (o == null)
421 {
422 Debug.LogWarning("Empty child track found while loading timeline. It will be removed.");
423 m_Children.RemoveAt(i);
424 }
425 }
426#endif
427 }
428
429 /// <summary>
430 /// Creates an AnimationClip to store animated properties for the attached PlayableAsset.
431 /// </summary>
432 /// <remarks>
433 /// If curves already exists for this track, this method produces no result regardless of
434 /// the value specified for curvesClipName.
435 /// </remarks>
436 /// <remarks>
437 /// When used from the editor, this method attempts to save the created curves clip to the TimelineAsset.
438 /// The TimelineAsset must already exist in the AssetDatabase to save the curves clip. If the TimelineAsset
439 /// does not exist, the curves clip is still created but it is not saved.
440 /// </remarks>
441 /// <param name="curvesClipName">
442 /// The name of the AnimationClip to create.
443 /// This method does not ensure unique names. If you want a unique clip name, you must provide one.
444 /// See ObjectNames.GetUniqueName for information on a method that creates unique names.
445 /// </param>
446 public void CreateCurves(string curvesClipName)
447 {
448 if (m_Curves != null)
449 return;
450
451 m_Curves = TimelineCreateUtilities.CreateAnimationClipForTrack(string.IsNullOrEmpty(curvesClipName) ? kDefaultCurvesName : curvesClipName, this, true);
452 }
453
454 /// <summary>
455 /// Creates a mixer used to blend playables generated by clips on the track.
456 /// </summary>
457 /// <param name="graph">The graph to inject playables into</param>
458 /// <param name="go">The GameObject that requested the graph.</param>
459 /// <param name="inputCount">The number of playables from clips that will be inputs to the returned mixer</param>
460 /// <returns>A handle to the [[Playable]] representing the mixer.</returns>
461 /// <remarks>
462 /// Override this method to provide a custom playable for mixing clips on a graph.
463 /// </remarks>
464 public virtual Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
465 {
466 return Playable.Create(graph, inputCount);
467 }
468
469 /// <summary>
470 /// Overrides PlayableAsset.CreatePlayable(). Not used in Timeline.
471 /// </summary>
472 /// <param name="graph"><inheritdoc/></param>
473 /// <param name="go"><inheritdoc/></param>
474 /// <returns><inheritDoc/></returns>
475 public sealed override Playable CreatePlayable(PlayableGraph graph, GameObject go)
476 {
477 return Playable.Null;
478 }
479
480 /// <summary>
481 /// Creates a TimelineClip on this track.
482 /// </summary>
483 /// <returns>Returns a new TimelineClip that is attached to the track.</returns>
484 /// <remarks>
485 /// The type of the playable asset attached to the clip is determined by TrackClip attributes that decorate the TrackAsset derived class
486 /// </remarks>
487 public TimelineClip CreateDefaultClip()
488 {
489 var trackClipTypeAttributes = GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
490 Type playableAssetType = null;
491 foreach (var trackClipTypeAttribute in trackClipTypeAttributes)
492 {
493 var attribute = trackClipTypeAttribute as TrackClipTypeAttribute;
494 if (attribute != null && typeof(IPlayableAsset).IsAssignableFrom(attribute.inspectedType) && typeof(ScriptableObject).IsAssignableFrom(attribute.inspectedType))
495 {
496 playableAssetType = attribute.inspectedType;
497 break;
498 }
499 }
500
501 if (playableAssetType == null)
502 {
503 Debug.LogWarning("Cannot create a default clip for type " + GetType());
504 return null;
505 }
506 return CreateAndAddNewClipOfType(playableAssetType);
507 }
508
509 /// <summary>
510 /// Creates a clip on the track with a playable asset attached, whose derived type is specified by T
511 /// </summary>
512 /// <typeparam name="T">A PlayableAsset derived type</typeparam>
513 /// <returns>Returns a TimelineClip whose asset is of type T</returns>
514 /// <remarks>
515 /// Throws <exception cref="System.InvalidOperationException"/> if <typeparamref name="T"/> is not supported by the track.
516 /// Supported types are determined by TrackClip attributes that decorate the TrackAsset derived class
517 /// </remarks>
518 public TimelineClip CreateClip<T>() where T : ScriptableObject, IPlayableAsset
519 {
520 return CreateClip(typeof(T));
521 }
522
523 /// <summary>
524 /// Delete a clip from this track.
525 /// </summary>
526 /// <param name="clip">The clip to delete.</param>
527 /// <returns>Returns true if the removal was successful</returns>
528 /// <remarks>
529 /// This method will delete a clip and any assets owned by the clip.
530 /// </remarks>
531 /// <exception>
532 /// Throws <exception cref="System.InvalidOperationException"/> if <paramref name="clip"/> is not a child of the TrackAsset.
533 /// </exception>
534 public bool DeleteClip(TimelineClip clip)
535 {
536 if (!m_Clips.Contains(clip))
537 throw new InvalidOperationException("Cannot delete clip since it is not a child of the TrackAsset.");
538
539 return timelineAsset != null && timelineAsset.DeleteClip(clip);
540 }
541
542 /// <summary>
543 /// Creates a marker of the requested type, at a specific time, and adds the marker to the current asset.
544 /// </summary>
545 /// <param name="type">The type of marker.</param>
546 /// <param name="time">The time where the marker is created.</param>
547 /// <returns>Returns the instance of the created marker.</returns>
548 /// <remarks>
549 /// All markers that implement IMarker and inherit from <see cref="UnityEngine.ScriptableObject"/> are supported.
550 /// Markers that implement the INotification interface cannot be added to tracks that do not support notifications.
551 /// CreateMarker will throw <exception cref="System.InvalidOperationException"/> with tracks that do not support notifications if <paramref name="type"/> implements the INotification interface.
552 /// </remarks>
553 /// <seealso cref="UnityEngine.Timeline.Marker"/>
554 /// <seealso cref="UnityEngine.Timeline.TrackAsset.supportsNotifications"/>
555 public IMarker CreateMarker(Type type, double time)
556 {
557 return m_Markers.CreateMarker(type, time, this);
558 }
559
560 /// <summary>
561 /// Creates a marker of the requested type, at a specific time, and adds the marker to the current asset.
562 /// </summary>
563 /// <param name="time">The time where the marker is created.</param>
564 /// <typeparam name="T">The type of marker to create.</typeparam>
565 /// <returns>Returns the instance of the created marker.</returns>
566 /// <remarks>
567 /// All markers that implement IMarker and inherit from <see cref="UnityEngine.ScriptableObject"/> are supported.
568 /// CreateMarker will throw <exception cref="System.InvalidOperationException"/> with tracks that do not support notifications if <typeparamref name="T"/> implements the INotification interface.
569 /// </remarks>
570 /// <seealso cref="UnityEngine.Timeline.Marker"/>
571 /// <seealso cref="UnityEngine.Timeline.TrackAsset.supportsNotifications"/>
572 public T CreateMarker<T>(double time) where T : ScriptableObject, IMarker
573 {
574 return (T)CreateMarker(typeof(T), time);
575 }
576
577 /// <summary>
578 /// Removes a marker from the current asset.
579 /// </summary>
580 /// <param name="marker">The marker instance to be removed.</param>
581 /// <returns>Returns true if the marker instance was successfully removed. Returns false otherwise.</returns>
582 public bool DeleteMarker(IMarker marker)
583 {
584 return m_Markers.Remove(marker);
585 }
586
587 /// <summary>
588 /// Returns an enumerable list of markers on the current asset.
589 /// </summary>
590 /// <returns>The list of markers on the asset.
591 /// </returns>
592 public IEnumerable<IMarker> GetMarkers()
593 {
594 return m_Markers.GetMarkers();
595 }
596
597 /// <summary>
598 /// Returns the number of markers on the current asset.
599 /// </summary>
600 /// <returns>The number of markers.</returns>
601 public int GetMarkerCount()
602 {
603 return m_Markers.Count;
604 }
605
606 /// <summary>
607 /// Returns the marker at a given position, on the current asset.
608 /// </summary>
609 /// <param name="idx">The index of the marker to be returned.</param>
610 /// <returns>The marker.</returns>
611 /// <remarks>The ordering of the markers is not guaranteed.
612 /// </remarks>
613 public IMarker GetMarker(int idx)
614 {
615 return m_Markers[idx];
616 }
617
618 internal TimelineClip CreateClip(System.Type requestedType)
619 {
620 if (ValidateClipType(requestedType))
621 return CreateAndAddNewClipOfType(requestedType);
622
623 throw new InvalidOperationException("Clips of type " + requestedType + " are not permitted on tracks of type " + GetType());
624 }
625
626 internal TimelineClip CreateAndAddNewClipOfType(Type requestedType)
627 {
628 var newClip = CreateClipOfType(requestedType);
629 AddClip(newClip);
630 return newClip;
631 }
632
633 internal TimelineClip CreateClipOfType(Type requestedType)
634 {
635 if (!ValidateClipType(requestedType))
636 throw new System.InvalidOperationException("Clips of type " + requestedType + " are not permitted on tracks of type " + GetType());
637
638 var playableAsset = CreateInstance(requestedType);
639 if (playableAsset == null)
640 {
641 throw new System.InvalidOperationException("Could not create an instance of the ScriptableObject type " + requestedType.Name);
642 }
643 playableAsset.name = requestedType.Name;
644 TimelineCreateUtilities.SaveAssetIntoObject(playableAsset, this);
645 TimelineUndo.RegisterCreatedObjectUndo(playableAsset, "Create Clip");
646
647 return CreateClipFromAsset(playableAsset);
648 }
649
650 /// <summary>
651 /// Creates a timeline clip from an existing playable asset.
652 /// </summary>
653 /// <param name="asset"></param>
654 /// <returns></returns>
655 internal TimelineClip CreateClipFromPlayableAsset(IPlayableAsset asset)
656 {
657 if (asset == null)
658 throw new ArgumentNullException("asset");
659
660 if ((asset as ScriptableObject) == null)
661 throw new System.ArgumentException("CreateClipFromPlayableAsset " + " only supports ScriptableObject-derived Types");
662
663 if (!ValidateClipType(asset.GetType()))
664 throw new System.InvalidOperationException("Clips of type " + asset.GetType() + " are not permitted on tracks of type " + GetType());
665
666 return CreateClipFromAsset(asset as ScriptableObject);
667 }
668
669 private TimelineClip CreateClipFromAsset(ScriptableObject playableAsset)
670 {
671 TimelineUndo.PushUndo(this, "Create Clip");
672
673 var newClip = CreateNewClipContainerInternal();
674 newClip.displayName = playableAsset.name;
675 newClip.asset = playableAsset;
676
677 IPlayableAsset iPlayableAsset = playableAsset as IPlayableAsset;
678 if (iPlayableAsset != null)
679 {
680 var candidateDuration = iPlayableAsset.duration;
681
682 if (!double.IsInfinity(candidateDuration) && candidateDuration > 0)
683 newClip.duration = Math.Min(Math.Max(candidateDuration, TimelineClip.kMinDuration), TimelineClip.kMaxTimeValue);
684 }
685
686 try
687 {
688 OnCreateClip(newClip);
689 }
690 catch (Exception e)
691 {
692 Debug.LogError(e.Message, playableAsset);
693 return null;
694 }
695
696 return newClip;
697 }
698
699 internal IEnumerable<ScriptableObject> GetMarkersRaw()
700 {
701 return m_Markers.GetRawMarkerList();
702 }
703
704 internal void ClearMarkers()
705 {
706 m_Markers.Clear();
707 }
708
709 internal void AddMarker(ScriptableObject e)
710 {
711 m_Markers.Add(e);
712 }
713
714 internal bool DeleteMarkerRaw(ScriptableObject marker)
715 {
716 return m_Markers.Remove(marker, timelineAsset, this);
717 }
718
719 int GetTimeRangeHash()
720 {
721 double start = double.MaxValue, end = double.MinValue;
722 int count = m_Markers.Count;
723 for (int i = 0; i < m_Markers.Count; i++)
724 {
725 var marker = m_Markers[i];
726 if (!(marker is INotification))
727 {
728 continue;
729 }
730
731 if (marker.time < start)
732 start = marker.time;
733 if (marker.time > end)
734 end = marker.time;
735 }
736
737 return start.GetHashCode().CombineHash(end.GetHashCode());
738 }
739
740 internal void AddClip(TimelineClip newClip)
741 {
742 if (!m_Clips.Contains(newClip))
743 {
744 m_Clips.Add(newClip);
745 m_ClipsCache = null;
746 }
747 }
748
749 Playable CreateNotificationsPlayable(PlayableGraph graph, Playable mixerPlayable, GameObject go, Playable timelinePlayable)
750 {
751 s_BuildData.markerList.Clear();
752 GatherNotifications(s_BuildData.markerList);
753
754 ScriptPlayable<TimeNotificationBehaviour> notificationPlayable;
755 if (go.TryGetComponent(out PlayableDirector director))
756 notificationPlayable = NotificationUtilities.CreateNotificationsPlayable(graph, s_BuildData.markerList, director);
757 else
758 notificationPlayable = NotificationUtilities.CreateNotificationsPlayable(graph, s_BuildData.markerList, timelineAsset);
759
760 if (notificationPlayable.IsValid())
761 {
762 notificationPlayable.GetBehaviour().timeSource = timelinePlayable;
763 if (mixerPlayable.IsValid())
764 {
765 notificationPlayable.SetInputCount(1);
766 graph.Connect(mixerPlayable, 0, notificationPlayable, 0);
767 notificationPlayable.SetInputWeight(mixerPlayable, 1);
768 }
769 }
770
771 return notificationPlayable;
772 }
773
774 internal Playable CreatePlayableGraph(PlayableGraph graph, GameObject go, IntervalTree<RuntimeElement> tree, Playable timelinePlayable)
775 {
776 UpdateDuration();
777 var mixerPlayable = Playable.Null;
778 if (CanCreateMixerRecursive())
779 mixerPlayable = CreateMixerPlayableGraph(graph, go, tree);
780
781 Playable notificationsPlayable = CreateNotificationsPlayable(graph, mixerPlayable, go, timelinePlayable);
782
783 // clear the temporary build data to avoid holding references
784 // case 1253974
785 s_BuildData.Clear();
786 if (!notificationsPlayable.IsValid() && !mixerPlayable.IsValid())
787 {
788 Debug.LogErrorFormat("Track {0} of type {1} has no notifications and returns an invalid mixer Playable", name,
789 GetType().FullName);
790
791 return Playable.Create(graph);
792 }
793
794 return notificationsPlayable.IsValid() ? notificationsPlayable : mixerPlayable;
795 }
796
797 internal virtual Playable CompileClips(PlayableGraph graph, GameObject go, IList<TimelineClip> timelineClips, IntervalTree<RuntimeElement> tree)
798 {
799 var blend = CreateTrackMixer(graph, go, timelineClips.Count);
800 for (var c = 0; c < timelineClips.Count; c++)
801 {
802 var source = CreatePlayable(graph, go, timelineClips[c]);
803 if (source.IsValid())
804 {
805 source.SetDuration(timelineClips[c].duration);
806 var clip = new RuntimeClip(timelineClips[c], source, blend);
807 tree.Add(clip);
808 graph.Connect(source, 0, blend, c);
809 blend.SetInputWeight(c, 0.0f);
810 }
811 }
812 ConfigureTrackAnimation(tree, go, blend);
813 return blend;
814 }
815
816 void GatherCompilableTracks(IList<TrackAsset> tracks)
817 {
818 if (!muted && CanCreateTrackMixer())
819 tracks.Add(this);
820
821 foreach (var c in GetChildTracks())
822 {
823 if (c != null)
824 c.GatherCompilableTracks(tracks);
825 }
826 }
827
828 void GatherNotifications(List<IMarker> markers)
829 {
830 if (!muted && CanCompileNotifications())
831 markers.AddRange(GetMarkers());
832 foreach (var c in GetChildTracks())
833 {
834 if (c != null)
835 c.GatherNotifications(markers);
836 }
837 }
838
839 internal virtual Playable CreateMixerPlayableGraph(PlayableGraph graph, GameObject go, IntervalTree<RuntimeElement> tree)
840 {
841 if (tree == null)
842 throw new ArgumentException("IntervalTree argument cannot be null", "tree");
843
844 if (go == null)
845 throw new ArgumentException("GameObject argument cannot be null", "go");
846
847 s_BuildData.Clear();
848 GatherCompilableTracks(s_BuildData.trackList);
849
850 // nothing to compile
851 if (s_BuildData.trackList.Count == 0)
852 return Playable.Null;
853
854 // check if layers are supported
855 Playable layerMixer = Playable.Null;
856 ILayerable layerable = this as ILayerable;
857 if (layerable != null)
858 layerMixer = layerable.CreateLayerMixer(graph, go, s_BuildData.trackList.Count);
859
860 if (layerMixer.IsValid())
861 {
862 for (int i = 0; i < s_BuildData.trackList.Count; i++)
863 {
864 var mixer = s_BuildData.trackList[i].CompileClips(graph, go, s_BuildData.trackList[i].clips, tree);
865 if (mixer.IsValid())
866 {
867 graph.Connect(mixer, 0, layerMixer, i);
868 layerMixer.SetInputWeight(i, 1.0f);
869 }
870 }
871 return layerMixer;
872 }
873
874 // one track compiles. Add track mixer and clips
875 if (s_BuildData.trackList.Count == 1)
876 return s_BuildData.trackList[0].CompileClips(graph, go, s_BuildData.trackList[0].clips, tree);
877
878 // no layer mixer provided. merge down all clips.
879 for (int i = 0; i < s_BuildData.trackList.Count; i++)
880 s_BuildData.clipList.AddRange(s_BuildData.trackList[i].clips);
881
882#if UNITY_EDITOR
883 bool applyWarning = false;
884 for (int i = 0; i < s_BuildData.trackList.Count; i++)
885 applyWarning |= i > 0 && s_BuildData.trackList[i].hasCurves;
886
887 if (applyWarning)
888 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.");
889#endif
890 // compile all the clips into a single mixer
891 return CompileClips(graph, go, s_BuildData.clipList, tree);
892 }
893
894 internal void ConfigureTrackAnimation(IntervalTree<RuntimeElement> tree, GameObject go, Playable blend)
895 {
896 if (!hasCurves)
897 return;
898
899 blend.SetAnimatedProperties(m_Curves);
900 tree.Add(new InfiniteRuntimeClip(blend));
901
902 if (OnTrackAnimationPlayableCreate != null)
903 OnTrackAnimationPlayableCreate.Invoke(this, go, blend);
904 }
905
906 // sorts clips by start time
907 internal void SortClips()
908 {
909 var clipsAsArray = clips; // will alloc
910 if (!m_CacheSorted)
911 {
912 Array.Sort(clips, (clip1, clip2) => clip1.start.CompareTo(clip2.start));
913 m_CacheSorted = true;
914 }
915 }
916
917 // clears the clips after a clone
918 internal void ClearClipsInternal()
919 {
920 m_Clips = new List<TimelineClip>();
921 m_ClipsCache = null;
922 }
923
924 internal void ClearSubTracksInternal()
925 {
926 m_Children = new List<ScriptableObject>();
927 Invalidate();
928 }
929
930 // called by an owned clip when it moves
931 internal void OnClipMove()
932 {
933 m_CacheSorted = false;
934 }
935
936 internal TimelineClip CreateNewClipContainerInternal()
937 {
938 var clipContainer = new TimelineClip(this);
939 clipContainer.asset = null;
940
941 // position clip at end of sequence
942 var newClipStart = 0.0;
943 for (var a = 0; a < m_Clips.Count - 1; a++)
944 {
945 var clipDuration = m_Clips[a].duration;
946 if (double.IsInfinity(clipDuration))
947 clipDuration = TimelineClip.kDefaultClipDurationInSeconds;
948 newClipStart = Math.Max(newClipStart, m_Clips[a].start + clipDuration);
949 }
950
951 clipContainer.mixInCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
952 clipContainer.mixOutCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
953 clipContainer.start = newClipStart;
954 clipContainer.duration = TimelineClip.kDefaultClipDurationInSeconds;
955 clipContainer.displayName = "untitled";
956 return clipContainer;
957 }
958
959 internal void AddChild(TrackAsset child)
960 {
961 if (child == null)
962 return;
963
964 m_Children.Add(child);
965 child.parent = this;
966 Invalidate();
967 }
968
969 internal void MoveLastTrackBefore(TrackAsset asset)
970 {
971 if (m_Children == null || m_Children.Count < 2 || asset == null)
972 return;
973
974 var lastTrack = m_Children[m_Children.Count - 1];
975 if (lastTrack == asset)
976 return;
977
978 for (int i = 0; i < m_Children.Count - 1; i++)
979 {
980 if (m_Children[i] == asset)
981 {
982 for (int j = m_Children.Count - 1; j > i; j--)
983 m_Children[j] = m_Children[j - 1];
984 m_Children[i] = lastTrack;
985 Invalidate();
986 break;
987 }
988 }
989 }
990
991 internal bool RemoveSubTrack(TrackAsset child)
992 {
993 if (m_Children.Remove(child))
994 {
995 Invalidate();
996 child.parent = null;
997 return true;
998 }
999 return false;
1000 }
1001
1002 internal void RemoveClip(TimelineClip clip)
1003 {
1004 m_Clips.Remove(clip);
1005 m_ClipsCache = null;
1006 }
1007
1008 // Is this track compilable for the sequence
1009 // calculate the time interval that this track will be evaluated in.
1010 internal virtual void GetEvaluationTime(out double outStart, out double outDuration)
1011 {
1012 outStart = 0;
1013 outDuration = 1;
1014
1015 outStart = double.PositiveInfinity;
1016 var outEnd = double.NegativeInfinity;
1017
1018 if (hasCurves)
1019 {
1020 outStart = 0.0;
1021 outEnd = TimeUtility.GetAnimationClipLength(curves);
1022 }
1023
1024 foreach (var clip in clips)
1025 {
1026 outStart = Math.Min(clip.start, outStart);
1027 outEnd = Math.Max(clip.end, outEnd);
1028 }
1029
1030 if (HasNotifications())
1031 {
1032 var notificationDuration = GetNotificationDuration();
1033 outStart = Math.Min(notificationDuration, outStart);
1034 outEnd = Math.Max(notificationDuration, outEnd);
1035 }
1036
1037 if (double.IsInfinity(outStart) || double.IsInfinity(outEnd))
1038 outStart = outDuration = 0.0;
1039 else
1040 outDuration = outEnd - outStart;
1041 }
1042
1043 // calculate the time interval that the sequence will use to determine length.
1044 // by default this is the same as the evaluation, but subclasses can have different
1045 // behaviour
1046 internal virtual void GetSequenceTime(out double outStart, out double outDuration)
1047 {
1048 GetEvaluationTime(out outStart, out outDuration);
1049 }
1050
1051 /// <summary>
1052 /// Called by the Timeline Editor to gather properties requiring preview.
1053 /// </summary>
1054 /// <param name="director">The PlayableDirector invoking the preview</param>
1055 /// <param name="driver">PropertyCollector used to gather previewable properties</param>
1056 public virtual void GatherProperties(PlayableDirector director, IPropertyCollector driver)
1057 {
1058 // only push on game objects if there is a binding. Subtracks
1059 // will use objects on the stack
1060 var gameObject = GetGameObjectBinding(director);
1061 if (gameObject != null)
1062 driver.PushActiveGameObject(gameObject);
1063
1064 if (hasCurves)
1065 driver.AddObjectProperties(this, m_Curves);
1066
1067 foreach (var clip in clips)
1068 {
1069 if (clip.curves != null && clip.asset != null)
1070 driver.AddObjectProperties(clip.asset, clip.curves);
1071
1072 IPropertyPreview modifier = clip.asset as IPropertyPreview;
1073 if (modifier != null)
1074 modifier.GatherProperties(director, driver);
1075 }
1076
1077 foreach (var subtrack in GetChildTracks())
1078 {
1079 if (subtrack != null)
1080 subtrack.GatherProperties(director, driver);
1081 }
1082
1083 if (gameObject != null)
1084 driver.PopActiveGameObject();
1085 }
1086
1087 internal GameObject GetGameObjectBinding(PlayableDirector director)
1088 {
1089 if (director == null)
1090 return null;
1091
1092 var binding = director.GetGenericBinding(this);
1093
1094 var gameObject = binding as GameObject;
1095 if (gameObject != null)
1096 return gameObject;
1097
1098 var comp = binding as Component;
1099 if (comp != null)
1100 return comp.gameObject;
1101
1102 return null;
1103 }
1104
1105 internal bool ValidateClipType(Type clipType)
1106 {
1107 var attrs = GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
1108 for (var c = 0; c < attrs.Length; ++c)
1109 {
1110 var attr = (TrackClipTypeAttribute)attrs[c];
1111 if (attr.inspectedType.IsAssignableFrom(clipType))
1112 return true;
1113 }
1114
1115 // special case for playable tracks, they accept all clips (in the runtime)
1116 return typeof(PlayableTrack).IsAssignableFrom(GetType()) &&
1117 typeof(IPlayableAsset).IsAssignableFrom(clipType) &&
1118 typeof(ScriptableObject).IsAssignableFrom(clipType);
1119 }
1120
1121 /// <summary>
1122 /// Called when a clip is created on a track.
1123 /// </summary>
1124 /// <param name="clip">The timeline clip added to this track</param>
1125 /// <remarks>Use this method to set default values on a timeline clip, or it's PlayableAsset.</remarks>
1126 protected virtual void OnCreateClip(TimelineClip clip) { }
1127
1128 void UpdateDuration()
1129 {
1130 // check if something changed in the clips that require a re-calculation of the evaluation times.
1131 var itemsHash = CalculateItemsHash();
1132 if (itemsHash == m_ItemsHash)
1133 return;
1134 m_ItemsHash = itemsHash;
1135
1136 double trackStart, trackDuration;
1137 GetSequenceTime(out trackStart, out trackDuration);
1138
1139 m_Start = (DiscreteTime)trackStart;
1140 m_End = (DiscreteTime)(trackStart + trackDuration);
1141
1142 // calculate the extrapolations time.
1143 // TODO Extrapolation time should probably be extracted from the SequenceClip so only a track is aware of it.
1144 this.CalculateExtrapolationTimes();
1145 }
1146
1147 protected internal virtual int CalculateItemsHash()
1148 {
1149 return HashUtility.CombineHash(GetClipsHash(), GetAnimationClipHash(m_Curves), GetTimeRangeHash());
1150 }
1151
1152 /// <summary>
1153 /// Constructs a Playable from a TimelineClip.
1154 /// </summary>
1155 /// <param name="graph">PlayableGraph that will own the playable.</param>
1156 /// <param name="gameObject">The GameObject that builds the PlayableGraph.</param>
1157 /// <param name="clip">The TimelineClip to construct a playable for.</param>
1158 /// <returns>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</returns>
1159 /// <exception cref="ArgumentException">Thrown if the specified PlayableGraph is not valid.</exception>
1160 /// <exception cref="ArgumentNullException">Thrown if the specified TimelineClip is not valid.</exception>
1161 /// <remarks>
1162 /// 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.
1163 /// </remarks>
1164 protected virtual Playable CreatePlayable(PlayableGraph graph, GameObject gameObject, TimelineClip clip)
1165 {
1166 if (!graph.IsValid())
1167 throw new ArgumentException("graph must be a valid PlayableGraph");
1168 if (clip == null)
1169 throw new ArgumentNullException("clip");
1170
1171 var asset = clip.asset as IPlayableAsset;
1172 if (asset != null)
1173 {
1174 var handle = asset.CreatePlayable(graph, gameObject);
1175 if (handle.IsValid())
1176 {
1177 handle.SetAnimatedProperties(clip.curves);
1178 handle.SetSpeed(clip.timeScale);
1179 if (OnClipPlayableCreate != null)
1180 OnClipPlayableCreate(clip, gameObject, handle);
1181 }
1182 return handle;
1183 }
1184 return Playable.Null;
1185 }
1186
1187 internal void Invalidate()
1188 {
1189 m_ChildTrackCache = null;
1190 var timeline = timelineAsset;
1191 if (timeline != null)
1192 {
1193 timeline.Invalidate();
1194 }
1195 }
1196
1197 internal double GetNotificationDuration()
1198 {
1199 if (!supportsNotifications)
1200 {
1201 return 0;
1202 }
1203
1204 var maxTime = 0.0;
1205 int count = m_Markers.Count;
1206 for (int i = 0; i < count; i++)
1207 {
1208 var marker = m_Markers[i];
1209 if (!(marker is INotification))
1210 {
1211 continue;
1212 }
1213 maxTime = Math.Max(maxTime, marker.time);
1214 }
1215
1216 return maxTime;
1217 }
1218
1219 internal virtual bool CanCompileClips()
1220 {
1221 return hasClips || hasCurves;
1222 }
1223
1224 /// <summary>
1225 /// Whether the track can create a mixer for its own contents.
1226 /// </summary>
1227 /// <returns>Returns true if the track's mixer should be included in the playable graph.</returns>
1228 /// <remarks>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</remarks>
1229 /// <remarks>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.</remarks>
1230 /// <remarks>Override this method to change the conditions for a track to be included in the playable graph.</remarks>
1231 public virtual bool CanCreateTrackMixer()
1232 {
1233 return CanCompileClips();
1234 }
1235
1236 internal bool IsCompilable()
1237 {
1238 bool isContainer = typeof(GroupTrack).IsAssignableFrom(GetType());
1239
1240 if (isContainer)
1241 return false;
1242
1243 var ret = !mutedInHierarchy && (CanCreateTrackMixer() || CanCompileNotifications());
1244 if (!ret)
1245 {
1246 foreach (var t in GetChildTracks())
1247 {
1248 if (t.IsCompilable())
1249 return true;
1250 }
1251 }
1252
1253 return ret;
1254 }
1255
1256 private void UpdateChildTrackCache()
1257 {
1258 if (m_ChildTrackCache == null)
1259 {
1260 if (m_Children == null || m_Children.Count == 0)
1261 m_ChildTrackCache = s_EmptyCache;
1262 else
1263 {
1264 var childTracks = new List<TrackAsset>(m_Children.Count);
1265 for (int i = 0; i < m_Children.Count; i++)
1266 {
1267 var subTrack = m_Children[i] as TrackAsset;
1268 if (subTrack != null)
1269 childTracks.Add(subTrack);
1270 }
1271 m_ChildTrackCache = childTracks;
1272 }
1273 }
1274 }
1275
1276 internal virtual int Hash()
1277 {
1278 return clips.Length + (m_Markers.Count << 16);
1279 }
1280
1281 int GetClipsHash()
1282 {
1283 var hash = 0;
1284 foreach (var clip in m_Clips)
1285 {
1286 hash = hash.CombineHash(clip.Hash());
1287 }
1288 return hash;
1289 }
1290
1291 /// <summary>
1292 /// Gets the hash code for an AnimationClip.
1293 /// </summary>
1294 /// <param name="clip">The animation clip.</param>
1295 /// <returns>A 32-bit signed integer that is the hash code for <paramref name="clip"/>. Returns 0 if <paramref name="clip"/> is null or empty.</returns>
1296 protected static int GetAnimationClipHash(AnimationClip clip)
1297 {
1298 var hash = 0;
1299 if (clip != null && !clip.empty)
1300 hash = hash.CombineHash(clip.frameRate.GetHashCode())
1301 .CombineHash(clip.length.GetHashCode());
1302
1303 return hash;
1304 }
1305
1306 bool HasNotifications()
1307 {
1308 return m_Markers.HasNotifications();
1309 }
1310
1311 bool CanCompileNotifications()
1312 {
1313 return supportsNotifications && m_Markers.HasNotifications();
1314 }
1315
1316 bool CanCreateMixerRecursive()
1317 {
1318 if (CanCreateTrackMixer())
1319 return true;
1320 foreach (var track in GetChildTracks())
1321 {
1322 if (track.CanCreateMixerRecursive())
1323 return true;
1324 }
1325
1326 return false;
1327 }
1328 }
1329}