Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.

Feedback playDirector.Pause(); -- not immediate

Discussion in 'Timeline' started by KarlKarl2000, Feb 12, 2020.

  1. KarlKarl2000

    KarlKarl2000

    Joined:
    Jan 25, 2016
    Posts:
    611
    Is anyone having problems where playDirector.Pause(); doesn't pause on the marker? It always pauses after 2-3 frames.

    This happens on both scaled and unscaled time. :eek:

    Thanks for the help!

    screenshot.5.jpg
     
  2. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    When timeline (or animation) plays it does not necessarily play on frame. It depends on the frame rate. Markers are triggered on the first frame sampled at or after the marker occurs.
     
  3. KarlKarl2000

    KarlKarl2000

    Joined:
    Jan 25, 2016
    Posts:
    611
    Thanks so much for the answer. Can you please suggest a way to catch the marker more accurately?

    Is there a way through C#?

    I'm using Timeline to animate 10 UI Rect Transforms scaling from 0 to an end value. Is that too frame rate intensive? :oops:

    Thanks
     
  4. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    Good question. If by 'catching the markers more accurately' you mean the timeline holds at the markers until you let it resume, there is a trick/workaround you can do.

    set the playable director to 'hold', then after hitting play, change the duration of the root playable of the graph.

    playableDirector.playableGraph.GetRootPlayable(0).SetDuration(firstMarkerTime);

    This will cause the timeline to not advance past the first marker time. It's akin to setting the timeline to a fixed duration in the editor.

    Then, when the marker is hit, or you detect the timeline is being held, change the duration to either the next marker or the actual duration of the timeline.

    The reason this works is the 'hold' mechanism will not let the time advance past that point. Other implementations would require back-tracking the timeline, which can be a problem if there are signals or other non-deterministic portions of the timeline.

    I hope that helps.
     
    ggzerosum likes this.
  5. KarlKarl2000

    KarlKarl2000

    Joined:
    Jan 25, 2016
    Posts:
    611
    hi @seant_unity

    Sorry for the late reply. Finally got time to try out your suggestion. It works very well and I'm able to catch each "marker" via C#. The actual Timeline markers are now empty placeholders (that don't use signal receivers), but are still very useful to accurately see each second to the 3rd decimal. (ie: firstMarkerTime, secondMarkerTime etc)

    Thanks so much for the help! :)
     
  6. Ferazel

    Ferazel

    Joined:
    Apr 18, 2010
    Posts:
    518
    This is a common use case for signals/markers and this is extremely problematic. If you have a pause marker on a timeline and it doesn't pause ON the marker, you end up with potentially other markers being executed as well that were supposed to execute after the pause.

    For example, let's say you have a timeline that controls a cutscene that is interrupted with dialog. You setup a marker for each point in the cutscene where characters will be talking. If the markers are too close together, if there is a frame rate hitch you will completely jump/skip over talk markers. This makes Timeline extremely dangerous and rife with bugs depending on a lot of situations out of the dev's control (frame rate on device, memory allocation/cleanup, etc.).

    This seems like a pretty common use case is there any way I can pause the timeline in a deterministic matter? I considered adding a flag to the marker receiver that would block all markers after the pause, but there's still the tricky situation of forcing the timeline BACK to when the pause occurred without processing the events prior.

    Does anyone have any recommendations for this (@seant_unity)?

    My ideal API would be something like:
    m_director.PauseAtTime(pauseMarker.time);
    This would block all previous markers from firing as well as any markers have yet to be processed after the current marker from firing (same frame markers would still fire once).
     
    Last edited: May 4, 2020
    CyRaid likes this.
  7. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    Yes - because timeline _samples_ based on a delta time, and it does not sweep, you will rarely land _exactly_ on a frame. Large delta time can be problematic.

    One approach I've posted before is to use the Hold feature. Set the playable director to extrapolation mode to hold, then, once the graph is Rebuilt, or the graph is playing, set the duration of the root playable to the next marker you want to hold at.

    playableDirector.playableGraph.GetRootPlayable(0).SetDuration(nextMarkerTime).

    Let the timeline 'hold' at that marker time until a condition is met, e.g. like a button press.

    At the last marker, put the original timeline duration back, and, if desired change the extrapolation mode to None.

    The advantage of doing it this way is trying to modify time after the timeline has crossed a boundary will have side-effects. Hold is the only way to prevent time from crossing a time boundary at all.
     
    Sarai, Jiaquarium and Jakky27 like this.
  8. Ferazel

    Ferazel

    Joined:
    Apr 18, 2010
    Posts:
    518
    After trying to work around the problem using bool flags and resetting the director time back I gave up and did what you recommended by setting the director's duration and setting the behavior to hold. I'm glad that there is something that we can do to have this behavior. However, I'd really appreciate a mechanic that allows timeline to do this behavior without the scripting support in the future (maybe some type of marker interface?). Thanks!
     
    LE-Peter and CyRaid like this.
  9. CyRaid

    CyRaid

    Joined:
    Mar 31, 2015
    Posts:
    134
    Exactly.. I'm not sure why internally you can't just sample t, then if there's a marker nearby, CHANGE t to the marked time and do processing after.
     
  10. Oneiros90

    Oneiros90

    Joined:
    Apr 29, 2014
    Posts:
    83
    I want to share my solution to this problem:
    1. Attach the provided TimelinePausesManager to the PlayableDirector
    2. Change your signals to be PauseSignals in your timeline
    The timeline will pause automatically and accurately to those signals.
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using UnityEngine;
    4. using UnityEngine.Events;
    5. using UnityEngine.Playables;
    6. using UnityEngine.Timeline;
    7.  
    8. namespace Utils
    9. {
    10.     // Special signal used to pause the timeline precisely
    11.     public class PauseSignalEmitter : SignalEmitter { }
    12.  
    13.     // Attach this component to the PlayableDirector in order to correctly setup the pauses
    14.     [RequireComponent(typeof(PlayableDirector))]
    15.     public class TimelinePausesManager : MonoBehaviour
    16.     {
    17.         private PlayableDirector _director;
    18.         private List<PauseSignalEmitter> _markers;
    19.  
    20.         private void Awake()
    21.         {
    22.             _director = GetComponent<PlayableDirector>();
    23.             _markers = GetAllMarkers<PauseSignalEmitter>(_director);
    24.  
    25.             if (_markers.Count > 0)
    26.                 SetupPauses();
    27.         }
    28.  
    29.         private void SetupPauses()
    30.         {
    31.             // Update timeline duration now and on any play/pause events
    32.             UpdateTimelineDuration();
    33.             _director.played += _ => UpdateTimelineDuration();
    34.             _director.paused += _ => UpdateTimelineDuration();
    35.  
    36.             // Make sure that the signal receiver exists
    37.             var signalReceiver = GetComponent<SignalReceiver>();
    38.             if (signalReceiver == null)
    39.                 signalReceiver = gameObject.AddComponent<SignalReceiver>();
    40.  
    41.             // Pause the timeline automatically on each pause marker
    42.             foreach (var marker in _markers)
    43.             {
    44.                 var reaction = signalReceiver.GetReaction(marker.asset);
    45.                 if (reaction == null)
    46.                 {
    47.                     reaction = new UnityEvent();
    48.                     signalReceiver.AddReaction(marker.asset, reaction);
    49.                 }
    50.                 reaction.AddListener(_director.Pause);
    51.             }
    52.         }
    53.  
    54.         private void UpdateTimelineDuration()
    55.         {
    56.             // Find the next pause marker that will be hit
    57.             var nextMarker = _markers
    58.                 .Where(m => m.time > _director.time)
    59.                 .FirstOrDefault();
    60.  
    61.             // Force the timeline duration to end exactly on that marker
    62.             // (or to the original duration if there are no pause markers ahead)
    63.             var nextMarkerTime = nextMarker != null ? nextMarker.time : _director.duration;
    64.             _director.playableGraph.GetRootPlayable(0).SetDuration(nextMarkerTime);
    65.         }
    66.  
    67.         /// Find all markers of a specified type
    68.         private static List<T> GetAllMarkers<T>(PlayableDirector director) where T : IMarker
    69.         {
    70.             List<T> markers = new();
    71.  
    72.             var timeline = director.playableAsset as TimelineAsset;
    73.             if (timeline == null)
    74.                 return markers;
    75.  
    76.             if (timeline.markerTrack == null)
    77.                 return markers;
    78.  
    79.             foreach (var marker in timeline.markerTrack.GetMarkers())
    80.                 if (marker is T tMarker)
    81.                     markers.Add(tMarker);
    82.  
    83.             return markers;
    84.         }
    85.     }
    86. }