Search Unity

Markers still evaluated when skipped even if no retroactive

Discussion in 'Timeline' started by Hazneliel, Oct 23, 2020.

  1. Hazneliel

    Hazneliel

    Joined:
    Nov 14, 2013
    Posts:
    305
    lets say I have a marker at position 10, but right at position 5 I call director.time = 15, this will make the timeline jump to position 15 and will skip the marker at position 10.

    Problem is that marker still gets evaluated as if it were hit at position 15, I even verified the flags of the marker doesn't include Retroactive.

    I appreciate any help.
    Thanks
     
  2. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    This is a known issue. The problem is the timeline is still playing and the jump in time crosses the signal boundary in a single frame. Retroactive is where the timeline starts at a time after a signal time.

    A workaround is to pause the timeline, set the time, then resume it. For example:

    Code (CSharp):
    1.     public void JumpTimeBy10Seconds()
    2.     {
    3.         var time = GetComponent<PlayableDirector>().time;
    4.         GetComponent<PlayableDirector>().Pause();
    5.         GetComponent<PlayableDirector>().time = time + 10;
    6.         GetComponent<PlayableDirector>().Play();
    7.     }
    8.  
     
    brian-nielsen and GileanVoid like this.
  3. Hazneliel

    Hazneliel

    Joined:
    Nov 14, 2013
    Posts:
    305
    Thanks, I dont think that would work because the order in which signals are processed when the graph is evaluated which is single threaded after lateUpdate. Meaning that pausing, changing time and resuming will happen in a single frame and then the other signal would hit and still be evaluated.

    I found another workaround, when processing the notification I check if the time it was triggered is the same time as the position of the signal, like this:

    Code (CSharp):
    1.    
    2. private static bool ShouldEvaluate(ZNETimeMachineMarker marker, double notificationTime) {
    3.        
    4.         // Check if this was evaluated at the position the marker is
    5.         // This is needed because when skipping it could be evaluated at the skip position instead.
    6.         if (Math.Abs(marker.time - notificationTime) < 0.1F) return true;
    7.        
    8.         return marker.flags.HasFlag(NotificationFlags.Retroactive);
    9.     }
    And then when receiving the notification just checking like this:

    Code (CSharp):
    1.    
    2. public void OnNotify(Playable origin, INotification notification, object context) {
    3.        
    4.         if (!Application.isPlaying) return;
    5.  
    6.         if (!(notification is ZNETimeMachineMarker marker)) return;
    7.        
    8.         var time = origin.IsValid() ? origin.GetTime() : 0.0F;
    9.  
    10.         if (!ShouldEvaluate(marker, time)) return;
    This has been working fine, I really hope Unity fixes this issue soon anyway.
     
    rosssssss likes this.
  4. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    Ok. I did build a small repo of the solution I posted and it worked correctly - the time jump wouldn't take effect until the next frame because Play is called after the graphs are already updated.

    However, if your solution works for your use case, great!

    As for a 'fix', whether the current implementation is incorrect is debatable - the timeline continues to play and crosses the signal boundary in a single frame. However, there may be something that we can do to easily allow the behavior you are seeking.
     
  5. LazloBonin

    LazloBonin

    Joined:
    Mar 6, 2015
    Posts:
    812
    Hi @seant_unity !

    I implemented an extension method called "Jump" that essentially implements your solution, with some additional bells and whistles (force-evaluating immediately, checking for whether the timeline is paused, etc.). This works well.

    Code (CSharp):
    1. public static void Jump(this PlayableDirector director, double time)
    2. {
    3.     EnsureArg.IsNotNull(director);
    4.    
    5.     var wasPlaying = director.state == PlayState.Playing;
    6.    
    7.     if (wasPlaying)
    8.     {
    9.         director.Pause();
    10.         director.time = time;
    11.         director.Evaluate();
    12.         director.Resume();
    13.     }
    14.     else
    15.     {
    16.         director.time = time;
    17.         director.Evaluate();
    18.     }
    19. }
    20.  

    However, the markers defined exactly on the target jump time do not seem to get triggered when a timeline resumes after a jump. I'm assuming the underlying logic is "play the marker if the time crosses the marker while playing", thus because we just resumed, that condition is false.

    I therefore need to find a way to trigger the markers manually.

    I wrote this super hacky piece of logic that works for now, but I'm wondering if you could advise something cleaner:

    Code (CSharp):
    1. public static void Jump(this PlayableDirector director, double time)
    2. {
    3.     EnsureArg.IsNotNull(director);
    4.    
    5.     var wasPlaying = director.state == PlayState.Playing;
    6.    
    7.     if (wasPlaying)
    8.     {
    9.         director.Pause();
    10.         director.time = time;
    11.         director.Evaluate();
    12.         director.Resume();
    13.  
    14.         // HACK: Unity does not trigger markers when resuming the timeline on the exact frame on which they are defined.
    15.         if (director.playableAsset is TimelineAsset timeline)
    16.         {
    17.             foreach (var marker in timeline.markerTrack.GetMarkers())
    18.             {
    19.                 if (marker is INotification notification && marker.time == time)
    20.                 {
    21.                     foreach (var directorReceiver in director.GetComponents<INotificationReceiver>())
    22.                     {
    23.                         directorReceiver.OnNotify(director.playableGraph.GetRootPlayable(0), notification, null);
    24.                     }
    25.                 }
    26.             }
    27.         }
    28.     }
    29.     else
    30.     {
    31.         director.time = time;
    32.         director.Evaluate();
    33.     }
    34. }
    35.  
    For example, this won't support markers on tracks other than the root track, which would be expected.

    (Also, I'm not sure which arguments to pass to playable and context of OnNotify).
     
  6. brian-nielsen

    brian-nielsen

    Joined:
    Apr 18, 2018
    Posts:
    15
    Bless you from the future :)
     
    MatthewGonzalezM likes this.
  7. rosssssss

    rosssssss

    Joined:
    Jan 27, 2017
    Posts:
    70
    The pause-set time-evaluate-play solution doesn't work for me - the OnNotify events still happen after all that has happened - I needed to use the Hazneliel solution