Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Question A problem while playing two playable directors sequentially.

Discussion in 'Timeline' started by mubarakalmehairbi-codya, Mar 30, 2024.

  1. mubarakalmehairbi-codya

    mubarakalmehairbi-codya

    Joined:
    Jul 13, 2023
    Posts:
    16
    Unity timeline is mainly designed for cutscenes that progress with time, not cutscenes that progress with player actions. There are multiple ways to deal with this issue, but I found that the most reliable way of dealing with this issue is to create a "master" script that runs the playable directors sequentially as can be seen in the image below. Once one playable director is finished, it will continue to loop or hold until the player does an action. The way my script know that the playable director has ended is through the
    PlayableDirector.stopped
    event (https://docs.unity3d.com/ScriptReference/Playables.PlayableDirector-stopped.html).
    However, there is one problem. In rare but consistent occasions, the animations in a playable director are affected by the previous playable director. For example: the 8th playable director in the sequence is affected by the 7th playable director even though the 7th playable director has supposedly stopped. My method of solving this is to wait for a fixed update before running the 8th playable director which makes the problem happen less frequently but it still happens. Why is a playable director affected by the previous playable director? Do I need to wait for a certain amount of time before running the next playable director? For your reference, I wrote the script below.

    upload_2024-3-30_21-48-3.png

    Code (CSharp):
    1.  
    2. public class Cutscene: MonoBehaviour
    3.     {
    4.         public UltEvent<Cutscene> OnStart; // UltEvent is similar to UnityEvent but with more features
    5.         public UltEvent<Cutscene> OnStop;
    6.         public UltEvent<Cutscene> OnComplete;
    7.  
    8.         public static Cutscene ActiveCutscene;
    9.         public List<CutscenePart> cutsceneParts = new List<CutscenePart>();
    10.         int partsIndex = 0;
    11.         bool forceEnd = false;
    12.         public bool IsCurrentCutscene {
    13.             get {
    14.                 return cutsceneParts[partsIndex].playableDir.playableGraph.IsValid() && cutsceneParts[partsIndex].playableDir.playableGraph.IsPlaying();
    15.             }
    16.         }
    17.         public bool disablePlayerControl = true;
    18.         public bool deactivateObjectUponStop = true;
    19.  
    20.         public IEnumerator TryToPlayNextPart() {
    21.             Debug.Log("Trying to play next part");
    22.             if (cutsceneParts[partsIndex].waitForFixedUpdateAfterStop) yield return new WaitForFixedUpdate();
    23.             if (partsIndex + 1 < cutsceneParts.Count) {
    24.                 partsIndex += 1;
    25.                 var currentPlayableDir = cutsceneParts[partsIndex].playableDir;
    26.                 cutsceneParts[partsIndex].playableDir.stopped += OnStopDirector;
    27.                 currentPlayableDir.gameObject.SetActive(true);
    28.                 currentPlayableDir.Play();
    29.                 currentPlayableDir.extrapolationMode = cutsceneParts[partsIndex].wrapMode;
    30.                 if (cutsceneParts[partsIndex].continueDialogue) {
    31.                     DialoguePopup.instance?.TryToContinue(); // This continues the dialogue in the cutscene
    32.                 }
    33.                 //yield return null;
    34.             } else {
    35.                 Debug.Log("Ending cutscene");
    36.                 //yield return null;
    37.                 gameObject.SetActive(!deactivateObjectUponStop);
    38.                 OnStop?.Invoke(this);
    39.                 OnComplete?.Invoke(this);
    40.                
    41.                 //yield return null;
    42.             }
    43.         }
    44.  
    45.         [ContextMenu("Play")]
    46.         public void Play() {
    47.             gameObject.SetActive(true);
    48.             forceEnd = false;
    49.             partsIndex = 0;
    50.             cutsceneParts[partsIndex].playableDir.extrapolationMode = cutsceneParts[partsIndex].wrapMode;
    51.             cutsceneParts[partsIndex].playableDir.stopped += OnStopDirector;
    52.             cutsceneParts[partsIndex].playableDir.gameObject.SetActive(true);
    53.             cutsceneParts[partsIndex].playableDir.Play();
    54.             ActiveCutscene = this;
    55.             OnStart?.Invoke(this);
    56.             if (cutsceneParts[partsIndex].continueDialogue) {
    57.                 DialoguePopup.instance?.TryToContinue();
    58.             }
    59.         }
    60.  
    61.         public void Pause() {
    62.             cutsceneParts[partsIndex].playableDir.playableGraph.GetRootPlayable(0).SetSpeed(0);
    63.         }
    64.  
    65.         public void EndLoop() {
    66.             cutsceneParts[partsIndex].playableDir.extrapolationMode = DirectorWrapMode.None;
    67.         }
    68.  
    69.         public void Continue() {
    70.             cutsceneParts[partsIndex].playableDir.playableGraph.GetRootPlayable(0).SetSpeed(1);
    71.             EndLoop();
    72.         }
    73.  
    74.         public void ForceEnd() {
    75.             forceEnd = true;
    76.             cutsceneParts[partsIndex].playableDir.Stop();
    77.            
    78.         }
    79.  
    80.         public void OnStopDirector(PlayableDirector playableDir_) {
    81.             cutsceneParts[partsIndex].playableDir.stopped -= OnStopDirector;
    82.             playableDir_.gameObject.SetActive(false);
    83.                 if (forceEnd) {
    84.                     forceEnd = false;
    85.                     Debug.Log("Ending cutscene");
    86.                     OnStop?.Invoke(this);
    87.                     gameObject.SetActive(!deactivateObjectUponStop);
    88.                 } else {
    89.                     StartCoroutine(TryToPlayNextPart());
    90.                 }
    91.         }
    92.     }
    93.  
    94.     [System.Serializable]
    95.     public class CutscenePart
    96.     {
    97.         public PlayableDirector playableDir;
    98.         public DirectorWrapMode wrapMode = DirectorWrapMode.None;
    99.         public bool continueDialogue = false;
    100.         public bool waitForFixedUpdateAfterStop = false;
    101.     }
     
  2. restush96

    restush96

    Joined:
    May 28, 2019
    Posts:
    149
    I use UniTask async and don't have the issue so far.
     
  3. mubarakalmehairbi-codya

    mubarakalmehairbi-codya

    Joined:
    Jul 13, 2023
    Posts:
    16
    Interesting, but how is this related?
     
  4. restush96

    restush96

    Joined:
    May 28, 2019
    Posts:
    149
    For me, using UniTask is because async await. Also, it can control Execution Order. Like early update, post update, so on.

    For your case:
    I'm not really sure, but I think it's because Timeline and IEnumerator has different Update. So, it may missed the order execution like too late or too early of one frame.

    I use Unitask just simple while loop like this.
    Code (CSharp):
    1.  
    2.                 async UniTask PlayCineAync()
    3.                 {
    4.                     PlayableDirector.time = startTime;
    5.  
    6.                     PlayableDirector.Play();
    7.                     while (Application.isPlaying)
    8.                     {
    9.                         if (Task.Status != Naninovel.Async.AwaiterStatus.Pending)
    10.                             break;
    11.                         await UniTask.Yield(asyncToken: asyncToken);
    12.                         if (asyncToken.Completed || asyncToken.Canceled)
    13.                             break;
    14.                         if (!ObjectUtils.IsValid(PlayableDirector) || !PlayableDirector.playableGraph.IsValid())
    15.                             break;
    16.  
    17.                         if (asyncToken.Canceled || asyncToken.Completed)
    18.                             break;
    19.                         if (PlayableDirector.state != PlayState.Playing)
    20.                             break;
    21.                     }
    22.                     Dispose();
    23.                     if (Task.Status == Naninovel.Async.AwaiterStatus.Pending)
    24.                         TrySetResult();
    25.                 }
    26.  
    The TrySetResult is from derived class UniTaskCompletionSource, would be called after they exited loop. Guaranteed not missed a frame, neither too late or too fast.