A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEngine.Playables;
4
5namespace UnityEngine.Timeline
6{
7 /// <summary>
8 /// Use this PlayableBehaviour to send notifications at a given time.
9 /// </summary>
10 /// <seealso cref="UnityEngine.Timeline.NotificationFlags"/>
11 public class TimeNotificationBehaviour : PlayableBehaviour
12 {
13 struct NotificationEntry
14 {
15 public double time;
16 public INotification payload;
17 public bool notificationFired;
18 public NotificationFlags flags;
19
20 public bool triggerInEditor
21 {
22 get { return (flags & NotificationFlags.TriggerInEditMode) != 0; }
23 }
24 public bool prewarm
25 {
26 get { return (flags & NotificationFlags.Retroactive) != 0; }
27 }
28 public bool triggerOnce
29 {
30 get { return (flags & NotificationFlags.TriggerOnce) != 0; }
31 }
32 }
33
34 readonly List<NotificationEntry> m_Notifications = new List<NotificationEntry>();
35 double m_PreviousTime;
36 bool m_NeedSortNotifications;
37
38 Playable m_TimeSource;
39
40 /// <summary>
41 /// Sets an optional Playable that provides duration and Wrap mode information.
42 /// </summary>
43 /// <remarks>
44 /// timeSource is optional. By default, the duration and Wrap mode will come from the current Playable.
45 /// </remarks>
46 public Playable timeSource
47 {
48 set { m_TimeSource = value; }
49 }
50
51 /// <summary>
52 /// Creates and initializes a ScriptPlayable with a TimeNotificationBehaviour.
53 /// </summary>
54 /// <param name="graph">The playable graph.</param>
55 /// <param name="duration">The duration of the playable.</param>
56 /// <param name="loopMode">The loop mode of the playable.</param>
57 /// <returns>A new TimeNotificationBehaviour linked to the PlayableGraph.</returns>
58 public static ScriptPlayable<TimeNotificationBehaviour> Create(PlayableGraph graph, double duration, DirectorWrapMode loopMode)
59 {
60 var notificationsPlayable = ScriptPlayable<TimeNotificationBehaviour>.Create(graph);
61 notificationsPlayable.SetDuration(duration);
62 notificationsPlayable.SetTimeWrapMode(loopMode);
63 notificationsPlayable.SetPropagateSetTime(true);
64 return notificationsPlayable;
65 }
66
67 /// <summary>
68 /// Adds a notification to be sent with flags, at a specific time.
69 /// </summary>
70 /// <param name="time">The time to send the notification.</param>
71 /// <param name="payload">The notification.</param>
72 /// <param name="flags">The notification flags that determine the notification behaviour. This parameter is set to Retroactive by default.</param>
73 /// <seealso cref="UnityEngine.Timeline.NotificationFlags"/>
74 public void AddNotification(double time, INotification payload, NotificationFlags flags = NotificationFlags.Retroactive)
75 {
76 m_Notifications.Add(new NotificationEntry
77 {
78 time = time,
79 payload = payload,
80 flags = flags
81 });
82 m_NeedSortNotifications = true;
83 }
84
85 /// <summary>
86 /// This method is called when the PlayableGraph that owns this PlayableBehaviour starts.
87 /// </summary>
88 /// <param name="playable">The reference to the playable associated with this PlayableBehaviour.</param>
89 public override void OnGraphStart(Playable playable)
90 {
91 SortNotifications();
92 var currentTime = playable.GetTime();
93 for (var i = 0; i < m_Notifications.Count; i++)
94 {
95 // case 1257208 - when a timeline is _resumed_, only reset notifications after the resumed time
96 if (m_Notifications[i].time > currentTime && !m_Notifications[i].triggerOnce)
97 {
98 var notification = m_Notifications[i];
99 notification.notificationFired = false;
100 m_Notifications[i] = notification;
101 }
102 }
103 m_PreviousTime = playable.GetTime();
104 }
105
106 /// <summary>
107 /// This method is called when the Playable play state is changed to PlayState.Paused
108 /// </summary>
109 /// <param name="playable">The reference to the playable associated with this PlayableBehaviour.</param>
110 /// <param name="info">Playable context information such as weight, evaluationType, and so on.</param>
111 public override void OnBehaviourPause(Playable playable, FrameData info)
112 {
113 if (playable.IsDone())
114 {
115 SortNotifications();
116 for (var i = 0; i < m_Notifications.Count; i++)
117 {
118 var e = m_Notifications[i];
119 if (!e.notificationFired)
120 {
121 var duration = playable.GetDuration();
122 var canTrigger = m_PreviousTime <= e.time && e.time <= duration;
123 if (canTrigger)
124 {
125 Trigger_internal(playable, info.output, ref e);
126 m_Notifications[i] = e;
127 }
128 }
129 }
130 }
131 }
132
133 /// <summary>
134 /// This method is called during the PrepareFrame phase of the PlayableGraph.
135 /// </summary>
136 /// <remarks>
137 /// Called once before processing starts.
138 /// </remarks>
139 /// <param name="playable">The reference to the playable associated with this PlayableBehaviour.</param>
140 /// <param name="info">Playable context information such as weight, evaluationType, and so on.</param>
141 public override void PrepareFrame(Playable playable, FrameData info)
142 {
143 // Never trigger on scrub
144 if (info.evaluationType == FrameData.EvaluationType.Evaluate)
145 {
146 return;
147 }
148
149 SyncDurationWithExternalSource(playable);
150 SortNotifications();
151 var currentTime = playable.GetTime();
152
153 // Fire notifications from previousTime till the end
154 if (info.timeLooped)
155 {
156 var duration = playable.GetDuration();
157 TriggerNotificationsInRange(m_PreviousTime, duration, info, playable, true);
158 var dx = playable.GetDuration() - m_PreviousTime;
159 var nFullTimelines = (int)((info.deltaTime * info.effectiveSpeed - dx) / playable.GetDuration());
160 for (var i = 0; i < nFullTimelines; i++)
161 {
162 TriggerNotificationsInRange(0, duration, info, playable, false);
163 }
164 TriggerNotificationsInRange(0, currentTime, info, playable, false);
165 }
166 else
167 {
168 var pt = playable.GetTime();
169 TriggerNotificationsInRange(m_PreviousTime, pt, info,
170 playable, true);
171 }
172
173 for (var i = 0; i < m_Notifications.Count; ++i)
174 {
175 var e = m_Notifications[i];
176 if (e.notificationFired && CanRestoreNotification(e, info, currentTime, m_PreviousTime))
177 {
178 Restore_internal(ref e);
179 m_Notifications[i] = e;
180 }
181 }
182
183 m_PreviousTime = playable.GetTime();
184 }
185
186 void SortNotifications()
187 {
188 if (m_NeedSortNotifications)
189 {
190 m_Notifications.Sort((x, y) => x.time.CompareTo(y.time));
191 m_NeedSortNotifications = false;
192 }
193 }
194
195 static bool CanRestoreNotification(NotificationEntry e, FrameData info, double currentTime, double previousTime)
196 {
197 if (e.triggerOnce)
198 return false;
199 if (info.timeLooped)
200 return true;
201
202 //case 1111595: restore the notification if the time is manually set before it
203 return previousTime > currentTime && currentTime <= e.time;
204 }
205
206 void TriggerNotificationsInRange(double start, double end, FrameData info, Playable playable, bool checkState)
207 {
208 if (start <= end)
209 {
210 var playMode = Application.isPlaying;
211 for (var i = 0; i < m_Notifications.Count; i++)
212 {
213 var e = m_Notifications[i];
214 if (e.notificationFired && (checkState || e.triggerOnce))
215 continue;
216
217 var notificationTime = e.time;
218 if (e.prewarm && notificationTime < end && (e.triggerInEditor || playMode))
219 {
220 Trigger_internal(playable, info.output, ref e);
221 m_Notifications[i] = e;
222 }
223 else
224 {
225 if (notificationTime < start || notificationTime > end)
226 continue;
227
228 if (e.triggerInEditor || playMode)
229 {
230 Trigger_internal(playable, info.output, ref e);
231 m_Notifications[i] = e;
232 }
233 }
234 }
235 }
236 }
237
238 void SyncDurationWithExternalSource(Playable playable)
239 {
240 if (m_TimeSource.IsValid())
241 {
242 playable.SetDuration(m_TimeSource.GetDuration());
243 playable.SetTimeWrapMode(m_TimeSource.GetTimeWrapMode());
244 }
245 }
246
247 static void Trigger_internal(Playable playable, PlayableOutput output, ref NotificationEntry e)
248 {
249 output.PushNotification(playable, e.payload);
250 e.notificationFired = true;
251 }
252
253 static void Restore_internal(ref NotificationEntry e)
254 {
255 e.notificationFired = false;
256 }
257 }
258}