Skip to content

Instantly share code, notes, and snippets.

@stonstad
Last active November 6, 2022 17:52
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save stonstad/7bdd805d52f43d6880cc073a48757aa9 to your computer and use it in GitHub Desktop.
Save stonstad/7bdd805d52f43d6880cc073a48757aa9 to your computer and use it in GitHub Desktop.
Unity Animation State Start and Stop Notifications
using System;
using System.Reflection;
using UnityEngine;
public static class AnimatorExtensions
{
/// <summary>Gets an instance method with single argument of type <typeparamref
/// name="TArg0"/> and return type of <typeparamref name="TReturn"/> from <typeparamref
/// name="TThis"/> and compiles it into a fast open delegate.</summary>
/// <typeparam name="TThis">Type of the class owning the instance method.</typeparam>
/// <typeparam name="TArg0">Type of the single parameter to the instance method to
/// find.</typeparam>
/// <typeparam name="TReturn">Type of the return for the method</typeparam>
/// <param name="methodName">The name of the method the compile.</param>
/// <returns>The compiled delegate, which should be about as fast as calling the function
/// directly on the instance.</returns>
/// <exception cref="ArgumentException">If the method can't be found, or it has an
/// unexpected return type (the return type must match exactly).</exception>
/// <see href="https://codeblog.jonskeet.uk/2008/08/09/making-reflection-fly-and-exploring-delegates/"/>
private static Func<TThis, TArg0, TReturn> BuildFastOpenMemberDelegate<TThis, TArg0, TReturn>(string methodName)
{
var method = typeof(TThis).GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
CallingConventions.Any,
new[] { typeof(TArg0) },
null);
if (method == null)
throw new ArgumentException("Can't find method " + typeof(TThis).FullName + "." + methodName + "(" + typeof(TArg0).FullName + ")");
else if (method.ReturnType != typeof(TReturn))
throw new ArgumentException("Expected " + typeof(TThis).FullName + "." + methodName + "(" + typeof(TArg0).FullName + ") to have return type of string but was " + method.ReturnType.FullName);
return (Func<TThis, TArg0, TReturn>)Delegate.CreateDelegate(typeof(Func<TThis, TArg0, TReturn>), method);
}
private static Func<Animator, int, string> _getCurrentStateName;
/// <summary>[FOR DEBUGGING ONLY] Calls an internal method on <see cref="Animator"/> that
/// returns the name of the current state for a layer. The internal method could be removed
/// or refactored at any time, and may not have good performance.</summary>
/// <param name="animator">The animator to get the current state from.</param>
/// <param name="layer">The layer to get the current state from.</param>
/// <returns>The name of the currently running state.</returns>
public static string GetCurrentStateName(this Animator animator, int layer)
{
if (_getCurrentStateName == null)
_getCurrentStateName = BuildFastOpenMemberDelegate<Animator, int, string>("GetCurrentStateName");
return _getCurrentStateName(animator, layer);
}
private static Func<Animator, int, string> _getNextStateName;
/// <summary>[FOR DEBUGGING ONLY] Calls an internal method on <see cref="Animator"/> that
/// returns the name of the next state for a layer. The internal method could be removed or
/// refactored at any time, and may not have good performance.</summary>
/// <param name="animator">The animator to get the next state from.</param>
/// <param name="layer">The layer to get the next state from.</param>
/// <returns>The name of the next running state.</returns>
public static string GetNextStateName(this Animator animator, int layer)
{
if (_getNextStateName == null)
_getNextStateName = BuildFastOpenMemberDelegate<Animator, int, string>("GetNextStateName");
return _getNextStateName(animator, layer);
}
private static Func<Animator, int, string> _resolveHash;
/// <summary>[FOR DEBUGGING ONLY] Calls an internal method on <see cref="Animator"/> that
/// returns the string used to create a hash from
/// <see cref="Animator.StringToHash(string)"/>. The internal method could be removed or
/// refactored at any time, and may not have good performance.</summary>
/// <param name="animator">The animator to get the string from.</param>
/// <param name="hash">The hash to get the original string for.</param>
/// <returns>The name of the string for <paramref name="hash"/>.</returns>
public static string ResolveHash(this Animator animator, int hash)
{
if (_resolveHash == null)
_resolveHash = BuildFastOpenMemberDelegate<Animator, int, string>("ResolveHash");
return _resolveHash(animator, hash);
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
namespace StellarConquest.Presentation.Unity
{
public class StateMachineBehaviorListener : StateMachineBehaviour
{
public event Action<int, string> OnStateEnterEvent;
public event Action<int, string> OnStateExitEvent;
public static Dictionary<int, string> States = new Dictionary<int, string>();
static StateMachineBehaviorListener()
{
AddState("Grounded");
AddState("Pickup");
AddState("Mine");
AddState("Downward Chop");
AddState("Horizontal Chop");
}
private static void AddState(string stateName)
{
States[Animator.StringToHash(stateName)] = stateName;
}
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
base.OnStateEnter(animator, stateInfo, layerIndex);
if (Valid(animator, stateInfo, layerIndex))
OnStateEnterEvent?.Invoke(stateInfo.shortNameHash, States[stateInfo.shortNameHash]);
}
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
base.OnStateExit(animator, stateInfo, layerIndex);
if (Valid(animator, stateInfo, layerIndex))
OnStateExitEvent?.Invoke(stateInfo.shortNameHash, States[stateInfo.shortNameHash]);
}
private bool Valid(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
#if (UNITY_EDITOR)
if (!States.ContainsKey(stateInfo.shortNameHash))
{
string stateName = animator.GetCurrentStateName(layerIndex);
Debug.LogError("State '" + stateName + "' missing.");
States[stateInfo.shortNameHash] = stateName;
}
return true;
#else
return States.ContainsKey(stateInfo.shortNameHash);
#endif
}
}
}
using System;
using UnityEngine;
using UnityEngine.Animations;
namespace Example
{
[RequireComponent(typeof(Animator))]
public class Usage : MonoBehaviour
{
private Animator _Animator;
private void Start()
{
_Animator = GetComponent<Animator>();
StateMachineBehaviorListener[] listeners = _Animator.GetBehaviours<StateMachineBehaviorListener>();
foreach (StateMachineBehaviorListener listener in listeners)
{
listener.OnStateEnterEvent += OnStateEnter;
listener.OnStateExitEvent += OnStateExit;
}
}
private void OnStateEnter(int stateHash, string stateName)
{
Debug.Log("ENTER " + stateName);
}
private void OnStateExit(int stateHash, string stateName)
{
Debug.Log("EXIT " + stateName);
}
}
}
@hyakugei
Copy link

FYI, your Valid method should pass the layerIndex param, and then use that in the GetCurrentStateName call. Thanks for the helpful code!!

@stonstad
Copy link
Author

FYI, your Valid method should pass the layerIndex param, and then use that in the GetCurrentStateName call. Thanks for the helpful code!!

Good call -- added!

@klaszlo8207
Copy link

Good code! Is there any way to check the percent of the transition?

Basically I have an Grounded animation clip and a GroundedWithGun in one transition (state)

How can I check the state time?

Grounded -> GroundedWithGun ---> 0.1...0.3.....1 DONE

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment