Extending StateMachineBehaviours

Hey guys, I’ve been working on a project which has involved a lot of StateMachineBehaviours and requires a lot of linking to things in the scene. As such I have made an extension to StateMachineBehaviour which helps with that. The you create a normal MonoBehaviour which has all your scene references and in it’s Start function it calls Initialise. Each of the StateMachineBehaviours you create should inherit from SceneLinkedSMB and use the MonoBehaviour you created as the generic type. So for example:

public class MyMonoBehaviour : MonoBehaviour
{
    public Rigidbody someSceneRigidbody;
    public Animator animator;

    void Start ()
    {
        SceneLinkedSMB<MyMonoBehaviour>.Initialise (animator, this);
    }
}

Then your StateMachineBehaviours would look like:

public class MySceneLinkedSMB : SceneLinkedSMB<MyMonoBehaviour>
{
    public override void OnSLStatePostEnter (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        m_MonoBehaviour.someSceneRigidbody.isKinematic = true;
    }
}

The SceneLinkedSMB also contains much more granular function calls so you can easily control exactly when things happen. The script itself contains an explanation of each of its functions.

using UnityEngine;
using UnityEngine.Animations;

public class SceneLinkedSMB<TMonoBehaviour> : SealedSMB 
    where TMonoBehaviour : MonoBehaviour
{
    protected TMonoBehaviour m_MonoBehaviour;
   
    bool m_FirstFrameHappened;
    bool m_LastFrameHappened;

    public static void Initialise (Animator animator, TMonoBehaviour monoBehaviour)
    {
        SceneLinkedSMB<TMonoBehaviour>[] sceneLinkedSMBs = animator.GetBehaviours<SceneLinkedSMB<TMonoBehaviour>>();

        for (int i = 0; i < sceneLinkedSMBs.Length; i++)
        {
            sceneLinkedSMBs[i].InternalInitialise(animator, monoBehaviour);
        }
    }

    protected void InternalInitialise (Animator animator, TMonoBehaviour monoBehaviour)
    {
        m_MonoBehaviour = monoBehaviour;
        OnStart (animator);
    }

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

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

    public sealed override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    {
        if (animator.IsInTransition(layerIndex) && animator.GetNextAnimatorStateInfo(layerIndex).fullPathHash == stateInfo.fullPathHash)
        {
            OnSLTransitionToStateUpdate(animator, stateInfo, layerIndex);
            OnSLTransitionToStateUpdate(animator, stateInfo, layerIndex, controller);
        }

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

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

        if (!animator.IsInTransition(layerIndex))
        {
            OnSLStateNoTransitionUpdate(animator, stateInfo, layerIndex);
            OnSLStateNoTransitionUpdate(animator, stateInfo, layerIndex, controller);
        }
       
        if (animator.IsInTransition(layerIndex) && !m_LastFrameHappened && m_FirstFrameHappened)
        {
            m_LastFrameHappened = true;

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

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

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

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

    /// <summary>
    /// Called by a MonoBehaviour in the scene during its Start function.
    /// </summary>
    public virtual void OnStart(Animator animator) { }

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

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

    /// <summary>
    /// Called every frame when the state is not being transitioned to or from.
    /// </summary>
    public virtual void OnSLStateNoTransitionUpdate(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 OnSLStatePreExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }

    /// <summary>
    /// Called after OnSLStatePreExit every frame during transition to the state.
    /// </summary>
    public virtual void OnSLTransitionFromStateUpdate(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 OnSLStateExit(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 OnSLStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called after OnSLStateEnter every frame during transition to the state.
    /// </summary>
    public virtual void OnSLTransitionToStateUpdate(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 OnSLStatePostEnter(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 OnSLStateNoTransitionUpdate(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 OnSLStatePreExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }

    /// <summary>
    /// Called after OnSLStatePreExit every frame during transition to the state.
    /// </summary>
    public virtual void OnSLTransitionFromStateUpdate(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 OnSLStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
}


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) { }
}

I hope you find this useful.

11 Likes

FYI, it’s actually a bit more hazardous than that. It’s not just if the transition’s duration is less than a frame, it’s if the transition finishes in less than a frame. This means that you can have a transition much longer than one frame, and still not always receive this callback.

If you have a 200ms transition, and a gameplay skip (a hard drive spinning up, plugs in a joystick and driver installation kicks in, or anything else that might cause a deltaTime hitch) causes Time.deltaTime to be 300, you can skip right over the transition and OnSLStatePreExit won’t be called. I tested this with a script that forces a gameplay skip by calling Thread.Sleep, and was able to skip over the PreExit call. That could lead to games ending up in an unintended state.

(Some awkward workarounds that could have unwanted side-effects: use manual Animator updates, and if Time.deltaTime is too big, break it apart and call update multiple times so no one update can be too big, or use animate physics.)

OnTransitionEnter and OnTransitionExit (and maybe OnTransitionInterrupted) callbacks would help this sort of thing a lot.

1 Like

We also noticed this problem. It seems impossible to get the association between a StateMachineBehaviour & its owning SM node which makes it rather troublesome for StateMachineBehaviours to get some decent composition going without extra book-keeping. I went another approach though: a behaviour whose only purpose is to give the parent animator enough context to trigger the correct callbacks & give us a set of the active behaviours ( OnStateEnter & OnStateExit is enough as long as it’s guaranteed that such a behaviour exist on all nodes ).

One of the upside is that we don’t have to do extra work inside OnStateUpdate. It also gives the same correctness guarantees as OnStateEnter, so hopefully doesn’t suffer from the delta > transition problem. The downside however is that it strongly relies on the fact that OnStateEnter is being called in the same order on behaviours as they show up on the nodes inside of the editor. This seems to be the case at the moment however.

It should always be the case, the order is:

  1. current state
  2. interrupted state(should only occur on the frame that the transition was interrupted)
  3. next state

Is there a way to know which state you’re coming from using the SceneLinkedSMB?

Something like a PreviousStateHash? Since not every state has to have a SceneLinkedSMB on it, it seems hard to know reliably in which state the SMB was.

1 Like

In the animator there are two states that are recorded at any one time: the current state and the next state. When transitioning from one state to another, the current state is the one being transitioned from and the next state is the one being transitioned to. This means in a normal OnStateUpdate call if you record the current state as a different state to the one the SMB is on then the state machine is in transition to the SMB’s state and the current state is the “previous” state you are looking for. From there it is simply a matter of recording it.

1 Like

The problem with this is that a StateMachineBehaviour is only aware of whichever state you put the script on. That means OnStateEnter, Update and Exit are called yes but in order to know the previous animation state you’d need to put a script on every state and you would probably also need to make a GetComponent call from the script to update a state manager.

My goal is quite simple: to put a single animator script that listens to when you enter/update/exit all the states of the entire Animator. If the AI behaviour is constrained to a single state like how I’ve seen most people use it in combination with the Animator, it makes the AI quite simple. Luckily all one needs to make a more complex AI is to know exactly when an animation state is being entered/updated/exitted and then you can hook that up in any complex system that you want.

I just found out that putting the script not on states but on the “ground” of the animator makes the script understand every state present in 1 layer of the Animator.

Here’s a gif to show what the “ground” of the animator means, I’m selecting with my mouse the ground and the Air state in this gif.
https://i.imgur.com/r4oPaEQ.gifv

Here’s the AnimatorListener that I’m using. It allows to know about every state in 1 layer,
the only problem I still have is that I can’t have access to other animator layers with that same script:

using System;
using UnityEngine;

// Put this on the topmost animation layer to get access to every animation played.
// Listens to any animation entered in the state machine.

public class AnimatorListener : StateMachineBehaviour
{
    public int LastVisual { get; private set; }
    public int CurrentVisual { get; private set; }

    /// <summary>
    /// DO NOT REGISTER ON AWAKE, IT ONLY WORKS IN START DUE TO INHERITING FROM STATEMACHINEBEHAVIOUR
    /// </summary>
    public Action<Animator, AnimatorStateInfo, int> OnEnterState;
    /// <summary>
    /// DO NOT REGISTER ON AWAKE, IT ONLY WORKS IN START DUE TO INHERITING FROM STATEMACHINEBEHAVIOUR
    /// </summary>
    public Action<Animator, AnimatorStateInfo, int> OnUpdateState;
    /// <summary>
    /// DO NOT REGISTER ON AWAKE, IT ONLY WORKS IN START DUE TO INHERITING FROM STATEMACHINEBEHAVIOUR
    /// </summary>
    public Action<Animator, AnimatorStateInfo, int> OnExitState;

    void Awake()
    {
        LastVisual = -1;
        CurrentVisual = -1;
    }

    public sealed override void OnStateEnter(Animator _animator, AnimatorStateInfo _animatorStateInfo, int _layerIndex)
    {
        if (OnEnterState != null)
        {
            OnEnterState.Invoke(_animator, _animatorStateInfo, _layerIndex);
        }

        LastVisual = CurrentVisual;
        CurrentVisual = _animatorStateInfo.shortNameHash;
    }

    public sealed override void OnStateUpdate(Animator _animator, AnimatorStateInfo _animatorStateInfo, int _layerIndex)
    {
        if (OnUpdateState != null)
            OnUpdateState.Invoke(_animator, _animatorStateInfo, _layerIndex);
    }

    public sealed override void OnStateExit(Animator _animator, AnimatorStateInfo _animatorStateInfo, int _layerIndex)
    {
        if (OnExitState != null)
            OnExitState.Invoke(_animator, _animatorStateInfo, _layerIndex);
    }
}

Please correct me if I’m wrong. And can I listen to all the states of all the layers in an Animator with a single script?

When you are adding the script the to the “ground” of the animator window you are adding it to the statemachine currently being displayed. This can be on any sub statemachines that have been created or the parent statemachine belonging to the layer (as in your case). I don’t think you can share one instance across multiple layers. However, you can share one instance across ALL animators using this attribute:

I recommend caution when using this though. It will make sure that only one instance of the SMB is created and it will be the instance that all animators reference. This might be what you want, it might not be. Note you will still need to add the script wherever you want to use it (each statemachine on each animator).

I hope this helps.

1 Like

It’s interesting but it’s not what I need, however you don’t have to mind it if there’s no solution, I’m quite content with what I have already. It was more of a curiosity.

Thanks James.

you need at least to put your script on every layer’s top most statemachine. In this case all sub state and sub statemachine inherit the script from their parent statemachine.

Don’t use SharedBetweenAnimators in this case because you do have some fields/properties in your script that aren’t static and they would have the same value for all your animators which won’t work since each animator can be in a different state

1 Like

I want to extend state machine behaviours so that they hold data of event informations.

For example, all my attack animations would need the event “ActivateHitbox” “Deactivate Hitbox”. So to create them faster than in the normal FBX importer I decided to store the information on a State within the Animator state itself.

This also solves the problem where I don’t want to lose my events every time I reimport an animation.

With an monobehaviour editor script I then read these behaviours and write the events in the read-only fbx.

#if UNITY_EDITOR
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

[RequireComponent(typeof(UnitEvents))]
[RequireComponent(typeof(Animator))]
public class AnimatorEventBaker : MonoBehaviour
{
    // When can exit
    // When can cancel/exit early
    // When can flip direction based on input x
    // When activate hitbox
    // When deactivate hitbox

    // Question how to go back to idle properly and give ample time to make another strike.

    public UnitEvents UnitEvents;

    public static bool ShowName = true;

    public AnimState[] States;

    [Button("Get from Animator")]
    private void GetEventsFromAnimator()
    {
        UnitEvents = GetComponent<UnitEvents>();
        var clips = AnimationUtility.GetAnimationClips(gameObject);
        var animator = GetComponent<Animator>();
        var behaviours = animator.GetBehaviours<AnimatorEvent>();

        var states = new List<AnimState>();

        for (var i = 0; i < behaviours.Length; i++)
        {
            behaviours[i].UnitEvents = UnitEvents;
            states.Add(behaviours[i].State);
            Debug.Log("state found: " + behaviours[i].State.Name);
            Debug.Log("State event: " + behaviours[i].UnitEvents);

//            EditorUtility.SetDirty(behaviours[i]);
        }

        foreach (var c in clips)
        {
            EditorUtility.SetDirty(c);
            Debug.Log("clip: " + c.name);
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        states = states.OrderBy(x => x.Name).ToList();

        States = states.ToArray();
    }

    public void Bake()
    {
        GetEventsFromAnimator();
        WriteEvents();
    }

    [Button("Bake")]
    private void WriteEvents()
    {
        // Get the clips
        var clips = AnimationUtility.GetAnimationClips(gameObject);

        for (int i = 0; i < clips.Length; i++)
        {
            var currentClip = clips[i];

            foreach (var state in States)
            {
                if (currentClip.name == state.Name)
                {
                    var events = new List<AnimationEvent>();
                    foreach (var ev in state.Events)
                    {
                        var target = ev.Target;
                        if (target == null) target = state.BaseTarget;
                        events.Add(CreateAnimationEvent(currentClip, target, ev.FunctionName, ev.NormalizedTime, ev.Int));
                    }

                    foreach (var ev in state.AttackEvents)
                    {
                        var target = (GameObject)state.BaseTarget;
                        var crap = target.GetComponent<Player>();
                        events.Add(CreateAnimationEvent(currentClip, crap, ev.FunctionName.ToString(), ev.NormalizedTime, ev.Nr));
                    }

                    AnimationUtility.SetAnimationEvents(currentClip, events.ToArray());
                    EditorUtility.SetDirty(currentClip);
                }

            }
        }


        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();


        Debug.Log("Bake finished");
    }

    [Button("Clear All")]
    private void CleanAllAnimatorEvents()
    {
        // Get the clips
        var clips = AnimationUtility.GetAnimationClips(gameObject);

        for (int i = 0; i < clips.Length; i++)
        {
            var currentClip = clips[i];

            CleanEventsFromClip(currentClip);
        }
    }

    private void CleanEventsFromClip(AnimationClip _clip)
    {
        AnimationUtility.SetAnimationEvents(_clip, new AnimationEvent[] { });
    }

    private AnimationEvent CreateAnimationEvent(AnimationClip _clip, Object _object, string _functionName,
        float _normalizedTime, int _int = 0)
    {
        float time2Trigger = _normalizedTime * _clip.length;
        AnimationEvent anEvent = new AnimationEvent
        {
            functionName = _functionName,
            objectReferenceParameter = _object,
            time = time2Trigger,
            intParameter = _int
        };

        return anEvent;
    }
}
#endif

The only problem I have is that my information is not being stored properly. Whenever I exit Unity, my events are not saved. How can I keep this information in the animation clips after quitting Unity? Why are the FBX animation clips read-only?

because if you modify them, on the next import your modification will be overriden.

So you have two options:

1 . you can duplicate your animation clips to edit them, I wouldn’t recommend this if you know that you will edit again your animation.
2. you can embedded the animation events in the model importer, like you do when you import an animation but from script.
see https://docs.unity3d.com/ScriptReference/ModelImporterClipAnimation-events.html
and https://docs.unity3d.com/ScriptReference/ModelImporter-clipAnimations.html

1 Like

How can I know on which state of my animator a StateMachineBehaviour script is? in order to get its motion (animation clip).
I need to know this @ editor time specifically (not in playmode). Is it possible?

@FeastSC2 When you say at Editor time, do you mean during play mode in the editor or just in the editor not during playmode?

I mean not during playmode, my goal is to preview my animations + fx in the scene view without starting playmode.

@FeastSC2 Check these out:

1 Like

It seems that this AnimatorController.FindStateMachineBehaviourContext does not work in combination with Animator.GetBehaviour<>. I checked several times and I don’t understand why this would not work, could it be a bug?
My intention is to have a StateManager component on the gameObject where the Animator is. I want to get all the StateMachineBehaviours in the animator and know what names these animator states have.

I used the sample (IdleBehaviour) from the documentation + the code below and it gives me an error on the line when calling context[0]

        Animator = GetComponent<Animator>();
        var test1 = Animator.GetBehaviour<IdleBehaviour>();
        if (test1 != null)
        {
            var context = AnimatorController.FindStateMachineBehaviourContext(test1);
            AnimatorState state = context[0].animatorObject as UnityEditor.Animations.AnimatorState;
            Debug.Log("state :" + state.name);
        }

@FeastSC2 I’m not sure I’m afraid. @Mecanim-Dev might know though.

@Mecanim-Dev Do you know why this is happening?

Yes, this is expected.

FindStateMachineBehaviourContext was made for the editor to allow to find in a controller where a specific SMB is located, since you can have the multiple instance of the same SMB class stored into different state we choosed to be more specific so this function is comparing instanceid to find the SMB context in a controller.

A controller is an asset and SMBs stored in a controller are also an asset in this case.

Animator.GetBehaviour returns runtime instance of the SMB, so that why they don’t match. You need to get the SMB from the controller since you are looking for the SMB asset and not the runtime instance.

What you want to do is

Animator = GetComponent<Animator>();
var animatorController = Animator.runtimeAnimatorController as AnimatorController;
var test1 = animatorController.GetBehaviour<IdleBehaviour>();
if (test1 != null)
{
    var context = AnimatorController.FindStateMachineBehaviourContext(test1);
    AnimatorState state = context[0].animatorObject as UnityEditor.Animations.AnimatorState;
    Debug.Log("state :" + state.name);
}
1 Like