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