Page 1 of 1

AnimatorPlayWait doesn't work if dialogue time scale is different from animator time scale

Posted: Mon Dec 18, 2023 10:45 am
by lgarczyn
The default dialogue time setting is "real time", or Time.unscaledDeltaTime

Most animators in most game run on Time.deltaTime, or Time.fixedDeltaTime

However in AnimatorPlayWait, the line

Code: Select all

yield return StartCoroutine(DialogueTime.WaitForSeconds(animatorStateInfo.length));
Waits for dialogue time instead of animator time, and thus terminates at the wrong time if Time.timeScale is different from 1.

Re: AnimatorPlayWait doesn't work if dialogue time scale is different from animator time scale

Posted: Mon Dec 18, 2023 10:56 am
by Tony Li
Hi,

Yes, that's correct; DialogueTime.WaitForSeconds observes the value of the current dialogue time setting.

If you want a different behavior, you could write a sequencer command that observes Time.timeScale (e.g., just copy SequencerCommandAnimatorPlayWait and change the yield return line), or change your animator to use unscaled time.

Re: AnimatorPlayWait doesn't work if dialogue time scale is different from animator time scale

Posted: Mon Dec 18, 2023 2:07 pm
by lgarczyn
I've changed the code to poll the animator instead of relying on timings

Code: Select all

// Copyright (c) Pixel Crushers. All rights reserved.

using UnityEngine;
using System.Collections;

namespace PixelCrushers.DialogueSystem.SequencerCommands
{

    /// <summary>
    /// Implements sequencer command: "AnimatorPlayWait(animatorParameter, [gameobject|speaker|listener], [crossfadeDuration], [layer])",
    /// which plays a state on a subject's Animator and waits until it's done.
    ///
    /// Fixes issues with AnimatorPlayWait by checking if the state is still playing in coroutine
    /// 
    /// Arguments:
    /// -# Name of a Mecanim animator state.
    /// -# (Optional) The subject; can be speaker, listener, or the name of a game object. Default: speaker.
    /// -# (Optional) Crossfade duration. Default: 0 (play immediately).
    /// -# (Optional) Animation Layer. Default: -1 (all layers).
    /// -# (Optional) Max anim duration. Default: 10 seconds.
    /// </summary>
    [AddComponentMenu("")] // Hide from menu.
    public class SequencerCommandAnimatorPlayWaitSmart : SequencerCommand
    {

        private const float maxDurationToWaitForStateStart = 1;
        private const float maxDurationToWaitForStateEnd = 10;

        public void Start()
        {

            // Get the values of the parameters:
            string stateName = GetParameter(0);
            Transform subject = GetSubject(1);
            float crossfadeDuration = GetParameterAsFloat(2);
            int layer = GetParameterAsInt(3, -1);
            float maxAnimDuration = GetParameterAsFloat(4, maxDurationToWaitForStateEnd);

            // Start the state:
            Animator animator = (subject != null) ? subject.GetComponentInChildren<Animator>() : null;
            if (animator == null)
            {
                if (DialogueDebug.logWarnings) Debug.Log(string.Format("{0}: Sequencer: AnimatorPlayWait({1}, {2}, fade={3}, layer={4}): No Animator found on {2}", new object[] { DialogueDebug.Prefix, stateName, (subject != null) ? subject.name : GetParameter(1), crossfadeDuration, layer }));
                Stop();
            }
            else
            {
                if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Sequencer: AnimatorPlayWait({1}, {2}, {3})", new object[] { DialogueDebug.Prefix, stateName, subject, crossfadeDuration }));
                if (!animator.gameObject.activeSelf) animator.gameObject.SetActive(true);
                if (Tools.ApproximatelyZero(crossfadeDuration))
                {
                    animator.Play(stateName, layer, 0);
                }
                else
                {
                    animator.CrossFadeInFixedTime(stateName, crossfadeDuration, layer);
                }
                StartCoroutine(MonitorState(animator, stateName, maxAnimDuration));
            }
        }

        /// <summary>
        /// Monitors the animator state. Stops the sequence when the state is done,
        /// or after maXDurationToWaitForStateStart (1 second) if it never enters
        /// that state.
        /// </summary>
        /// <param name="animator">Animator.</param>
        /// <param name="stateName">State name.</param>
        /// <param name="maxAnimDuration"></param>
        private IEnumerator MonitorState(Animator animator, string stateName, float maxAnimDuration)
        {

            // Wait to enter the state:
            float maxStartTime = DialogueTime.time + maxDurationToWaitForStateStart;
            AnimatorStateInfo animatorStateInfo;
            bool isInState = CheckIsInState(animator, stateName, out animatorStateInfo);
            while (!isInState && DialogueTime.time < maxStartTime)
            {
                yield return null;
                isInState = CheckIsInState(animator, stateName, out animatorStateInfo);
            }

            float maxEndTime = DialogueTime.time + maxAnimDuration;

            // Wait for state to end, then stop:
            while (isInState && DialogueTime.time < maxEndTime)
            {
                yield return null;
                isInState = CheckIsInState(animator, stateName, out animatorStateInfo);
            }
            Stop();
        }

        /// <summary>
        /// Checks if the animator is in a specified state on any of its layers, and
        /// returns the state info.
        /// </summary>
        /// <returns><c>true</c>, if in the state, <c>false</c> otherwise.</returns>
        /// <param name="animator">Animator.</param>
        /// <param name="stateName">State name.</param>
        /// <param name="animatorStateInfo">(Out) Animator state info.</param>
        private bool CheckIsInState(Animator animator, string stateName, out AnimatorStateInfo animatorStateInfo)
        {
            if (animator != null)
            {
                for (int layerIndex = 0; layerIndex < animator.layerCount; layerIndex++)
                {
                    AnimatorStateInfo info = animator.GetCurrentAnimatorStateInfo(layerIndex);
                    if (info.IsName(stateName))
                    {
                        animatorStateInfo = info;
                        return true;
                    }
                }
            }
            animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
            return false;
        }

    }

}

Re: AnimatorPlayWait doesn't work if dialogue time scale is different from animator time scale

Posted: Mon Dec 18, 2023 3:24 pm
by Tony Li
Hi,

Thanks for sharing!