A problem while playing two playable directors sequentially.

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 (Unity - Scripting API: Playables.PlayableDirector.stopped).
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.

public class Cutscene: MonoBehaviour
    {
        public UltEvent<Cutscene> OnStart; // UltEvent is similar to UnityEvent but with more features
        public UltEvent<Cutscene> OnStop;
        public UltEvent<Cutscene> OnComplete;

        public static Cutscene ActiveCutscene;
        public List<CutscenePart> cutsceneParts = new List<CutscenePart>();
        int partsIndex = 0;
        bool forceEnd = false;
        public bool IsCurrentCutscene {
            get {
                return cutsceneParts[partsIndex].playableDir.playableGraph.IsValid() && cutsceneParts[partsIndex].playableDir.playableGraph.IsPlaying();
            }
        }
        public bool disablePlayerControl = true;
        public bool deactivateObjectUponStop = true;

        public IEnumerator TryToPlayNextPart() {
            Debug.Log("Trying to play next part");
            if (cutsceneParts[partsIndex].waitForFixedUpdateAfterStop) yield return new WaitForFixedUpdate();
            if (partsIndex + 1 < cutsceneParts.Count) {
                partsIndex += 1;
                var currentPlayableDir = cutsceneParts[partsIndex].playableDir;
                cutsceneParts[partsIndex].playableDir.stopped += OnStopDirector;
                currentPlayableDir.gameObject.SetActive(true);
                currentPlayableDir.Play();
                currentPlayableDir.extrapolationMode = cutsceneParts[partsIndex].wrapMode;
                if (cutsceneParts[partsIndex].continueDialogue) {
                    DialoguePopup.instance?.TryToContinue(); // This continues the dialogue in the cutscene
                }
                //yield return null;
            } else {
                Debug.Log("Ending cutscene");
                //yield return null;
                gameObject.SetActive(!deactivateObjectUponStop);
                OnStop?.Invoke(this);
                OnComplete?.Invoke(this);
               
                //yield return null;
            }
        }

        [ContextMenu("Play")]
        public void Play() {
            gameObject.SetActive(true);
            forceEnd = false;
            partsIndex = 0;
            cutsceneParts[partsIndex].playableDir.extrapolationMode = cutsceneParts[partsIndex].wrapMode;
            cutsceneParts[partsIndex].playableDir.stopped += OnStopDirector;
            cutsceneParts[partsIndex].playableDir.gameObject.SetActive(true);
            cutsceneParts[partsIndex].playableDir.Play();
            ActiveCutscene = this;
            OnStart?.Invoke(this);
            if (cutsceneParts[partsIndex].continueDialogue) {
                DialoguePopup.instance?.TryToContinue();
            }
        }

        public void Pause() {
            cutsceneParts[partsIndex].playableDir.playableGraph.GetRootPlayable(0).SetSpeed(0);
        }

        public void EndLoop() {
            cutsceneParts[partsIndex].playableDir.extrapolationMode = DirectorWrapMode.None;
        }

        public void Continue() {
            cutsceneParts[partsIndex].playableDir.playableGraph.GetRootPlayable(0).SetSpeed(1);
            EndLoop();
        }

        public void ForceEnd() {
            forceEnd = true;
            cutsceneParts[partsIndex].playableDir.Stop();
           
        }

        public void OnStopDirector(PlayableDirector playableDir_) {
            cutsceneParts[partsIndex].playableDir.stopped -= OnStopDirector;
            playableDir_.gameObject.SetActive(false);
                if (forceEnd) {
                    forceEnd = false;
                    Debug.Log("Ending cutscene");
                    OnStop?.Invoke(this);
                    gameObject.SetActive(!deactivateObjectUponStop);
                } else {
                    StartCoroutine(TryToPlayNextPart());
                }
        }
    }

    [System.Serializable]
    public class CutscenePart
    {
        public PlayableDirector playableDir;
        public DirectorWrapMode wrapMode = DirectorWrapMode.None;
        public bool continueDialogue = false;
        public bool waitForFixedUpdateAfterStop = false;
    }

I use UniTask async and don’t have the issue so far.

Interesting, but how is this related?

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.

                async UniTask PlayCineAync()
                {
                    PlayableDirector.time = startTime;

                    PlayableDirector.Play();
                    while (Application.isPlaying)
                    {
                        if (Task.Status != Naninovel.Async.AwaiterStatus.Pending)
                            break;
                        await UniTask.Yield(asyncToken: asyncToken);
                        if (asyncToken.Completed || asyncToken.Canceled)
                            break;
                        if (!ObjectUtils.IsValid(PlayableDirector) || !PlayableDirector.playableGraph.IsValid())
                            break;

                        if (asyncToken.Canceled || asyncToken.Completed)
                            break;
                        if (PlayableDirector.state != PlayState.Playing)
                            break;
                    }
                    Dispose();
                    if (Task.Status == Naninovel.Async.AwaiterStatus.Pending)
                        TrySetResult();
                }

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.

1 Like