Entry state always uses default transition and ignores other conditional transitions (Bug?)

I absolutely feel everyone’s pain here. I’ve had this issue continually over the years and it drives me nuts! It would be nice for the Default Entry Transition to act as a fallback (which seems like is the intended use case), but there are times where it suddenly decides that it takes precedence over all other transitions. I have yet another solution that likely won’t work in all cases, but wanted to share it here in case it can help anyone else:

TL;DR - I delete all transitions from the entry node in my Sub State Machine and use a StateMachineBehaviour to manually cross fade to the appropriate animations based on an int parameter I’ve set up in the Animator Controller.

So, my Sub State Machine looks like this with no transitions from the entry node to any of my states. Obviously, we can’t do anything about the orange default entry transition, but it doesn’t matter with this method.
6725836--773929--upload_2021-1-14_12-47-38.png

I attach the StateChooserSMB script to the Sub State Machine and fill out the fields.

Int Param Name: The name of an integer parameter in the Animator Controller used to select which state we transition to next.

State Prefix: For this system to work, I’m using Animator.CrossFade which requires the FULL path to the state in order to work. So, in this example, we’ll need ‘Base Layer.Talking.Talking Loops.’ appended to the front of all our state names.

State Names: The names of all the states in our Sub State Machine. There’s no (quick) way to access these dynamically as far as I know, so you just have to type out all your state names again. If anyone has suggestions on how to populate this list dynamically, I would be very happy.

And that’s it for setup! Now let’s talk scripts:

StateChooserSMB.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StateChooserSMB : AdvancedSMB
{
    //The name of the integer parameter in the Animator Controller used to select which state we transition
    //to next.
    [SerializeField]
    private string _intParamName;
    //For this to work, we're using Animator.CrossFade which requires the FULL path to the state in order to work.
    //So, in this example, we'll need 'Base Layer.Talking.Talking Loops.' appended to the front of all our state
    //names.
    [SerializeField]
    private string _statePrefix;
    //An array of all the state names in this sub state machine.
    [SerializeField]
    private string[] _stateNames;

    private int[] _stateHashes;

    //The last time OnStateEnter was called.
    private float _lastStateEnterTime;
    private float _timeoutTime;
    private int _indexParamHash;

    //The duration of the transition between states. You can set this up as an inspector variable if you want.
    private static readonly float TRANSITION_DURATION = 0.25f;

    private void Awake()
    {
        _indexParamHash = Animator.StringToHash(_intParamName);
        int stateCount = _stateNames.Length;
        _stateHashes = new int[stateCount];

        for (int i = 0; i < stateCount; i++)
        {
            string statePath = _statePrefix + _stateNames[i];
            _stateHashes[i] = Animator.StringToHash(statePath);
        }
    }

    public override void OnAdvancedStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        //State enter was getting called multiple times. So when this function gets called we store
        //off the current time.
        float currentTime = Time.timeSinceLevelLoad;

        //If the current time is less than our timeout time, then we don't want to do anything at the moment.
        //We only want this function to do anything once the timeout is up.
        if (currentTime < _timeoutTime)
            return;

        //If we make it down here, then we're outside of the timeout window. So we update some variables to
        //start the next timeout period.
        _lastStateEnterTime = Time.timeSinceLevelLoad;
        _timeoutTime = _lastStateEnterTime + TRANSITION_DURATION;

        //Each of the states in our Sub State Machine is associated with an integer value. That integer value
        //is getting set on our animator by an external C# script. So we access that value.
        int intParamValue = animator.GetInteger(_indexParamHash);

        //And then we use that value to access the proper state in our state array.
        animator.CrossFadeInFixedTime(_stateHashes[intParamValue], TRANSITION_DURATION);
    }
}

That script inherits from AdvancedSMB.cs which is a modification of the extremely helpful script written by @JamesB found here: Extending StateMachineBehaviours

Here’s AdvancedSMB.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;

/// <summary>
/// An advanced State Machine Behavior class that gives us better control over when certain events fire.
/// Pulled from here basically wholesale https://discussions.unity.com/t/674264
/// </summary>
public class AdvancedSMB : SealedSMB
{
    private bool m_FirstFrameHappened;
    private bool m_LastFrameHappened;

    public sealed override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    {
        m_FirstFrameHappened = false;

        OnAdvancedStateEnter(animator, stateInfo, layerIndex);
        OnAdvancedStateEnter(animator, stateInfo, layerIndex, controller);
    }

    public sealed override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    {
        if (!animator.gameObject.activeSelf)
            return;

        if (animator.IsInTransition(layerIndex) && animator.GetNextAnimatorStateInfo(layerIndex).fullPathHash == stateInfo.fullPathHash)
        {
            OnAdvancedTransitionToStateUpdate(animator, stateInfo, layerIndex);
            OnAdvancedTransitionToStateUpdate(animator, stateInfo, layerIndex, controller);
        }

        if (!animator.IsInTransition(layerIndex) && m_FirstFrameHappened)
        {
            OnAdvancedStateNoTransitionUpdate(animator, stateInfo, layerIndex);
            OnAdvancedStateNoTransitionUpdate(animator, stateInfo, layerIndex, controller);
        }

        if (animator.IsInTransition(layerIndex) && !m_LastFrameHappened && m_FirstFrameHappened)
        {
            m_LastFrameHappened = true;

            OnAdvancedStatePreExit(animator, stateInfo, layerIndex);
            OnAdvancedStatePreExit(animator, stateInfo, layerIndex, controller);
        }

        if (!animator.IsInTransition(layerIndex) && !m_FirstFrameHappened)
        {
            m_FirstFrameHappened = true;

            OnAdvancedStatePostEnter(animator, stateInfo, layerIndex);
            OnAdvancedStatePostEnter(animator, stateInfo, layerIndex, controller);
        }

        if (animator.IsInTransition(layerIndex) && animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash == stateInfo.fullPathHash)
        {
            OnAdvancedTransitionFromStateUpdate(animator, stateInfo, layerIndex);
            OnAdvancedTransitionFromStateUpdate(animator, stateInfo, layerIndex, controller);
        }
    }

    public sealed override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    {
        m_LastFrameHappened = false;

        OnAdvancedStateExit(animator, stateInfo, layerIndex);
        OnAdvancedStateExit(animator, stateInfo, layerIndex, controller);
    }


    /// <summary>
    /// Called before Updates when execution of the state first starts (on transition to the state).
    /// </summary>
    public virtual void OnAdvancedStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called after OnAdvancedStateEnter every frame during transition to the state.
    /// </summary>
    public virtual void OnAdvancedTransitionToStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called on the first frame after the transition to the state has finished.
    /// </summary>
    public virtual void OnAdvancedStatePostEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called every frame after PostEnter when the state is not being transitioned to or from.
    /// </summary>
    public virtual void OnAdvancedStateNoTransitionUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called on the first frame after the transition from the state has started.  Note that if the transition has a duration of less than a frame, this will not be called.
    /// </summary>
    public virtual void OnAdvancedStatePreExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called after OnAdvancedStatePreExit every frame during transition to the state.
    /// </summary>
    public virtual void OnAdvancedTransitionFromStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called after Updates when execution of the state first finshes (after transition from the state).
    /// </summary>
    public virtual void OnAdvancedStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called before Updates when execution of the state first starts (on transition to the state).
    /// </summary>
    public virtual void OnAdvancedStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called after OnAdvancedStateEnter every frame during transition to the state.
    /// </summary>
    public virtual void OnAdvancedTransitionToStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called on the first frame after the transition to the state has finished.
    /// </summary>
    public virtual void OnAdvancedStatePostEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called every frame when the state is not being transitioned to or from.
    /// </summary>
    public virtual void OnAdvancedStateNoTransitionUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called on the first frame after the transition from the state has started.  Note that if the transition has a duration of less than a frame, this will not be called.
    /// </summary>
    public virtual void OnAdvancedStatePreExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called after OnAdvancedStatePreExit every frame during transition to the state.
    /// </summary>
    public virtual void OnAdvancedTransitionFromStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called after Updates when execution of the state first finshes (after transition from the state).
    /// </summary>
    public virtual void OnAdvancedStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
}

And its parent class SealedSMB.cs

using UnityEngine;

public abstract class SealedSMB : StateMachineBehaviour
{
    public sealed override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    public sealed override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    public sealed override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
}

Hope this is helpful to anyone! Good luck :slight_smile:

1 Like