using System; using System.Collections.Generic; using UnityEngine.Playables; namespace UnityEngine.Timeline { /// /// Use this PlayableBehaviour to send notifications at a given time. /// /// 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 m_Notifications = new List(); double m_PreviousTime; bool m_NeedSortNotifications; Playable m_TimeSource; /// /// Sets an optional Playable that provides duration and Wrap mode information. /// /// /// timeSource is optional. By default, the duration and Wrap mode will come from the current Playable. /// public Playable timeSource { set { m_TimeSource = value; } } /// /// Creates and initializes a ScriptPlayable with a TimeNotificationBehaviour. /// /// The playable graph. /// The duration of the playable. /// The loop mode of the playable. /// A new TimeNotificationBehaviour linked to the PlayableGraph. public static ScriptPlayable Create(PlayableGraph graph, double duration, DirectorWrapMode loopMode) { var notificationsPlayable = ScriptPlayable.Create(graph); notificationsPlayable.SetDuration(duration); notificationsPlayable.SetTimeWrapMode(loopMode); notificationsPlayable.SetPropagateSetTime(true); return notificationsPlayable; } /// /// Adds a notification to be sent with flags, at a specific time. /// /// The time to send the notification. /// The notification. /// The notification flags that determine the notification behaviour. This parameter is set to Retroactive by default. /// 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; } /// /// This method is called when the PlayableGraph that owns this PlayableBehaviour starts. /// /// The reference to the playable associated with this PlayableBehaviour. public override void OnGraphStart(Playable playable) { SortNotifications(); for (var i = 0; i < m_Notifications.Count; i++) { var notification = m_Notifications[i]; notification.notificationFired = false; m_Notifications[i] = notification; } m_PreviousTime = playable.GetTime(); } /// /// This method is called when the Playable play state is changed to PlayState.Paused /// /// The reference to the playable associated with this PlayableBehaviour. /// Playable context information such as weight, evaluationType, and so on. 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; } } } } } /// /// This method is called during the PrepareFrame phase of the PlayableGraph. /// /// /// Called once before processing starts. /// /// The reference to the playable associated with this PlayableBehaviour. /// Playable context information such as weight, evaluationType, and so on. 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; } } }