Page 1 of 1

Subtitle Panel and Unscaled Time

Posted: Fri Oct 15, 2021 4:38 pm
by VoodooDetective
I was just curious, is there an important reason the subtitle panel animators work with unscaled time?

Our {{default}} sequence waits for the Subtitle panel to open (I have Sequencer.Message("SubtitleOpened")) to make sure the panel is open before dialogue starts. But if I pause the game as the panel is opening, then the message hits the Sequencer when it's paused and so the message is discarded hanging the game.

I was going to switch to scaled time for those animators, but I just wanted to make sure that wouldn't break something else.

Re: Subtitle Panel and Unscaled Time

Posted: Fri Oct 15, 2021 5:35 pm
by Tony Li
Hi,

Some games pause time during conversations. In those cases, the animators need to be set to unscaled time.

Otherwise, it's perfectly fine to set it to normal time if that works best for your project.

Re: Subtitle Panel and Unscaled Time

Posted: Fri Oct 15, 2021 8:46 pm
by VoodooDetective
OK great! So would your recommendation be to comment out this line in UIAnimatorMonitor:

Code: Select all

CheckAnimatorModeAndTimescale(triggerName);
in WaitForAnimation()?


EDIT:
Huh, even if I do that and it uses Normal time, I'm having that issue pop up. I'm sending the Sequencer message from a coroutine that starts in ShowSubtitle and waits for the panel to open:

Code: Select all

        private IEnumerator OnPanelOpen(params Action[] onOpenCallbacks)
        {
            while (!hasFocus && panelState != PanelState.Open)
            {
                yield return null;
            }
            yield return waitForEndOfFrame;
            foreach (Action action in onOpenCallbacks) action?.Invoke();
        }
The sequencer message is one of the callbacks.


Edit 2:
Ahh, looks like there's a safety valve kind of thing in StandardUISubtitlePanel:

Code: Select all

        protected IEnumerator FocusWhenOpen()
        {
            float timeout = Time.realtimeSinceStartup + 5f;
            while (panelState != PanelState.Open && Time.realtimeSinceStartup < timeout)
            {
                yield return null;
            }
            m_focusWhenOpenCoroutine = null;
            FocusNow();
        }
That causes FocusNow() to be called even if the panel isn't open yet.

Edit 3:
AHHH, but even if I comment out the safety valve, the panel gets set to open before the animation has finished. Digging more.

Edit 4:
Ahhhhhh OK, so there's another place unscaled time is forced and also where there is another safety valve in UIAnimatorMonitor:

Code: Select all

        private IEnumerator WaitForAnimation(string triggerName, System.Action callback, bool wait)
        {
            if (HasAnimator() && !string.IsNullOrEmpty(triggerName))
            {
                if (IsAnimatorValid())
                {
                    // Run Animator and wait:
                    CheckAnimatorModeAndTimescale(triggerName);
                    m_animator.SetTrigger(triggerName);
                    currentTrigger = triggerName;
                    float timeout = Time.realtimeSinceStartup + MaxWaitDuration + 100000;
                    var goalHashID = Animator.StringToHash(triggerName);
                    var oldHashId = UIUtility.GetAnimatorNameHash(m_animator.GetCurrentAnimatorStateInfo(0));
                    var currentHashID = oldHashId;
                    if (wait)
                    {
                        while ((currentHashID != goalHashID) && (currentHashID == oldHashId) && (Time.realtimeSinceStartup < timeout))
                        {
                            yield return null;
                            currentHashID = IsAnimatorValid() ? UIUtility.GetAnimatorNameHash(m_animator.GetCurrentAnimatorStateInfo(0)) : 0;
                        }
                        if (Time.realtimeSinceStartup < timeout && IsAnimatorValid())
                        {
                            var clipLength = m_animator.GetCurrentAnimatorStateInfo(0).length;
                             if (Mathf.Approximately(0, Time.timeScale))
                             {
                                 timeout = Time.realtimeSinceStartup + clipLength;
                                 while (Time.realtimeSinceStartup < timeout)
                                 {
                                     yield return null;
                                 }
                            }
                            else
                            {
                                yield return new WaitForSeconds(clipLength);
                            }
                        }
                    }
                }
                else if (m_animation != null && m_animation.enabled)
                {
                    m_animation.Play(triggerName);
                    if (wait)
                    {
                        var clip = m_animation.GetClip(triggerName);
                        if (clip != null)
                        {
                            yield return new WaitForSeconds(clip.length);
                        }
                    }
                }
            }
            currentTrigger = string.Empty;
            m_coroutine = null;
            if (callback != null) callback.Invoke();
        }

If I comment out all the safety valves, the code that forces unscaled time, AND set all animators to normal, then the issue goes away. But that seems like a pretty heavy handed approach. Do you have any better ideas?

I wonder if this animation functionality could be abstracted out into an interface that could have this stuff as the default implementation, and then allow folks to implement their own. Like with a library like animancer or something. Still digging...


Edit 5:
OK, so you may have a better idea, but what would you think of:

Code: Select all

namespace PixelCrushers
{
    public interface IUIAnimatorMonitor
    {
        string currentTrigger { get; }
        void SetTrigger(string triggerName, System.Action callback, bool wait = true);
        void CancelCurrentAnimation();
    }
}
and then in UIPanel:

Code: Select all

        public virtual IUIAnimatorMonitor animatorMonitor
        {
            get
            {
                if (m_animatorMonitor == null) m_animatorMonitor = new UIAnimatorMonitor(gameObject);
                return m_animatorMonitor;
            }
        }

and then in UIAnimatorMonitor:

Code: Select all

    public class UIAnimatorMonitor : IUIAnimatorMonitor
and then finally in StandardUISubtitlePanel:

Code: Select all

        protected IEnumerator FocusWhenOpen()
        {
            // float timeout = Time.realtimeSinceStartup + 5f;
            while (panelState != PanelState.Open)// && Time.realtimeSinceStartup < timeout)
            {
                yield return null;
            }
            m_focusWhenOpenCoroutine = null;
            FocusNow();
        }

Re: Subtitle Panel and Unscaled Time

Posted: Sat Oct 16, 2021 12:36 am
by Tony Li
I think something similar to that would work. The intent of the realtimeSinceStartup check is to prevent situations where the animator is waiting forever for an animation (and thus hanging the rest of the Dialogue System) because an animator's states or transitions aren't set up right. But, if you're using normal time, I suppose it should not even try to start the animation if time is paused -- in which case, it wouldn't get to the loop that checks realtimeSinceStartup. I'll take a look at your suggestions and what I can do to make it a little easier to work with in normal time.

Re: Subtitle Panel and Unscaled Time

Posted: Mon Oct 18, 2021 1:53 pm
by VoodooDetective
Awesome, thank you! That change I made is working, but I don't know if it's the cleanest. You may have a better idea. Just let me know if I can do anything, or if you need a tester :)

I'll keep an eye out for changes here or in the release notes. Thanks again!