playDirector.Pause(); -- not immediate

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. :hushed:

Thanks for the help!

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.

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? :sweat_smile:

Thanks

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.

1 Like

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! :slight_smile:

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).

1 Like

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.

3 Likes

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!

2 Likes

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.

I want to share my solution to this problem:

  • Attach the provided TimelinePausesManager to the PlayableDirector
  • Change your signals to be PauseSignals in your timeline

The timeline will pause automatically and accurately to those signals.

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace Utils
{
    // Special signal used to pause the timeline precisely
    public class PauseSignalEmitter : SignalEmitter { }

    // Attach this component to the PlayableDirector in order to correctly setup the pauses
    [RequireComponent(typeof(PlayableDirector))]
    public class TimelinePausesManager : MonoBehaviour
    {
        private PlayableDirector _director;
        private List<PauseSignalEmitter> _markers;

        private void Awake()
        {
            _director = GetComponent<PlayableDirector>();
            _markers = GetAllMarkers<PauseSignalEmitter>(_director);

            if (_markers.Count > 0)
                SetupPauses();
        }

        private void SetupPauses()
        {
            // Update timeline duration now and on any play/pause events
            UpdateTimelineDuration();
            _director.played += _ => UpdateTimelineDuration();
            _director.paused += _ => UpdateTimelineDuration();

            // Make sure that the signal receiver exists
            var signalReceiver = GetComponent<SignalReceiver>();
            if (signalReceiver == null)
                signalReceiver = gameObject.AddComponent<SignalReceiver>();

            // Pause the timeline automatically on each pause marker
            foreach (var marker in _markers)
            {
                var reaction = signalReceiver.GetReaction(marker.asset);
                if (reaction == null)
                {
                    reaction = new UnityEvent();
                    signalReceiver.AddReaction(marker.asset, reaction);
                }
                reaction.AddListener(_director.Pause);
            }
        }

        private void UpdateTimelineDuration()
        {
            // Find the next pause marker that will be hit
            var nextMarker = _markers
                .Where(m => m.time > _director.time)
                .FirstOrDefault();

            // Force the timeline duration to end exactly on that marker
            // (or to the original duration if there are no pause markers ahead)
            var nextMarkerTime = nextMarker != null ? nextMarker.time : _director.duration;
            _director.playableGraph.GetRootPlayable(0).SetDuration(nextMarkerTime);
        }

        /// Find all markers of a specified type
        private static List<T> GetAllMarkers<T>(PlayableDirector director) where T : IMarker
        {
            List<T> markers = new();

            var timeline = director.playableAsset as TimelineAsset;
            if (timeline == null)
                return markers;

            if (timeline.markerTrack == null)
                return markers;

            foreach (var marker in timeline.markerTrack.GetMarkers())
                if (marker is T tMarker)
                    markers.Add(tMarker);

            return markers;
        }
    }
}
6 Likes