Search Unity

Extending StateMachineBehaviours

Discussion in 'Animation' started by JamesB, Aug 15, 2017.

Thread Status:
Not open for further replies.
  1. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    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:

    Code (csharp):
    1. public class MyMonoBehaviour : MonoBehaviour
    2. {
    3.     public Rigidbody someSceneRigidbody;
    4.     public Animator animator;
    5.  
    6.     void Start ()
    7.     {
    8.         SceneLinkedSMB<MyMonoBehaviour>.Initialise (animator, this);
    9.     }
    10. }
    Then your StateMachineBehaviours would look like:

    Code (csharp):
    1. public class MySceneLinkedSMB : SceneLinkedSMB<MyMonoBehaviour>
    2. {
    3.     public override void OnSLStatePostEnter (Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    4.     {
    5.         m_MonoBehaviour.someSceneRigidbody.isKinematic = true;
    6.     }
    7. }
    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.

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEngine.Animations;
    3.  
    4. public class SceneLinkedSMB<TMonoBehaviour> : SealedSMB
    5.     where TMonoBehaviour : MonoBehaviour
    6. {
    7.     protected TMonoBehaviour m_MonoBehaviour;
    8.    
    9.     bool m_FirstFrameHappened;
    10.     bool m_LastFrameHappened;
    11.  
    12.     public static void Initialise (Animator animator, TMonoBehaviour monoBehaviour)
    13.     {
    14.         SceneLinkedSMB<TMonoBehaviour>[] sceneLinkedSMBs = animator.GetBehaviours<SceneLinkedSMB<TMonoBehaviour>>();
    15.  
    16.         for (int i = 0; i < sceneLinkedSMBs.Length; i++)
    17.         {
    18.             sceneLinkedSMBs[i].InternalInitialise(animator, monoBehaviour);
    19.         }
    20.     }
    21.  
    22.     protected void InternalInitialise (Animator animator, TMonoBehaviour monoBehaviour)
    23.     {
    24.         m_MonoBehaviour = monoBehaviour;
    25.         OnStart (animator);
    26.     }
    27.  
    28.     public sealed override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    29.     {
    30.         m_FirstFrameHappened = false;
    31.  
    32.         OnSLStateEnter(animator, stateInfo, layerIndex);
    33.         OnSLStateEnter (animator, stateInfo, layerIndex, controller);
    34.     }
    35.  
    36.     public sealed override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    37.     {
    38.         if (animator.IsInTransition(layerIndex) && animator.GetNextAnimatorStateInfo(layerIndex).fullPathHash == stateInfo.fullPathHash)
    39.         {
    40.             OnSLTransitionToStateUpdate(animator, stateInfo, layerIndex);
    41.             OnSLTransitionToStateUpdate(animator, stateInfo, layerIndex, controller);
    42.         }
    43.  
    44.         if (!animator.IsInTransition(layerIndex) && !m_FirstFrameHappened)
    45.         {
    46.             m_FirstFrameHappened = true;
    47.  
    48.             OnSLStatePostEnter(animator, stateInfo, layerIndex);
    49.             OnSLStatePostEnter(animator, stateInfo, layerIndex, controller);
    50.         }
    51.  
    52.         if (!animator.IsInTransition(layerIndex))
    53.         {
    54.             OnSLStateNoTransitionUpdate(animator, stateInfo, layerIndex);
    55.             OnSLStateNoTransitionUpdate(animator, stateInfo, layerIndex, controller);
    56.         }
    57.        
    58.         if (animator.IsInTransition(layerIndex) && !m_LastFrameHappened && m_FirstFrameHappened)
    59.         {
    60.             m_LastFrameHappened = true;
    61.  
    62.             OnSLStatePreExit(animator, stateInfo, layerIndex);
    63.             OnSLStatePreExit(animator, stateInfo, layerIndex, controller);
    64.         }
    65.  
    66.         if (animator.IsInTransition(layerIndex) && animator.GetCurrentAnimatorStateInfo(layerIndex).fullPathHash == stateInfo.fullPathHash)
    67.         {
    68.             OnSLTransitionFromStateUpdate(animator, stateInfo, layerIndex);
    69.             OnSLTransitionFromStateUpdate(animator, stateInfo, layerIndex, controller);
    70.         }
    71.     }
    72.  
    73.     public sealed override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller)
    74.     {
    75.         m_LastFrameHappened = false;
    76.  
    77.         OnSLStateExit(animator, stateInfo, layerIndex);
    78.         OnSLStateExit(animator, stateInfo, layerIndex, controller);
    79.     }
    80.  
    81.     /// <summary>
    82.     /// Called by a MonoBehaviour in the scene during its Start function.
    83.     /// </summary>
    84.     public virtual void OnStart(Animator animator) { }
    85.  
    86.     /// <summary>
    87.     /// Called before Updates when execution of the state first starts (on transition to the state).
    88.     /// </summary>
    89.     public virtual void OnSLStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    90.    
    91.     /// <summary>
    92.     /// Called after OnSLStateEnter every frame during transition to the state.
    93.     /// </summary>
    94.     public virtual void OnSLTransitionToStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    95.  
    96.     /// <summary>
    97.     /// Called on the first frame after the transition to the state has finished.
    98.     /// </summary>
    99.     public virtual void OnSLStatePostEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    100.  
    101.     /// <summary>
    102.     /// Called every frame when the state is not being transitioned to or from.
    103.     /// </summary>
    104.     public virtual void OnSLStateNoTransitionUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    105.  
    106.     /// <summary>
    107.     /// 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.
    108.     /// </summary>
    109.     public virtual void OnSLStatePreExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    110.  
    111.     /// <summary>
    112.     /// Called after OnSLStatePreExit every frame during transition to the state.
    113.     /// </summary>
    114.     public virtual void OnSLTransitionFromStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    115.  
    116.     /// <summary>
    117.     /// Called after Updates when execution of the state first finshes (after transition from the state).
    118.     /// </summary>
    119.     public virtual void OnSLStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    120.  
    121.     /// <summary>
    122.     /// Called before Updates when execution of the state first starts (on transition to the state).
    123.     /// </summary>
    124.     public virtual void OnSLStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    125.  
    126.     /// <summary>
    127.     /// Called after OnSLStateEnter every frame during transition to the state.
    128.     /// </summary>
    129.     public virtual void OnSLTransitionToStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    130.  
    131.     /// <summary>
    132.     /// Called on the first frame after the transition to the state has finished.
    133.     /// </summary>
    134.     public virtual void OnSLStatePostEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    135.  
    136.     /// <summary>
    137.     /// Called every frame when the state is not being transitioned to or from.
    138.     /// </summary>
    139.     public virtual void OnSLStateNoTransitionUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    140.  
    141.     /// <summary>
    142.     /// 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.
    143.     /// </summary>
    144.     public virtual void OnSLStatePreExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    145.  
    146.     /// <summary>
    147.     /// Called after OnSLStatePreExit every frame during transition to the state.
    148.     /// </summary>
    149.     public virtual void OnSLTransitionFromStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    150.  
    151.     /// <summary>
    152.     /// Called after Updates when execution of the state first finshes (after transition from the state).
    153.     /// </summary>
    154.     public virtual void OnSLStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex, AnimatorControllerPlayable controller) { }
    155. }
    156.  
    157.  
    158. public abstract class SealedSMB : StateMachineBehaviour
    159. {
    160.     public sealed override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    161.  
    162.     public sealed override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    163.  
    164.     public sealed override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { }
    165. }
    I hope you find this useful.
     
  2. NoiseFloorDev

    NoiseFloorDev

    Joined:
    May 13, 2017
    Posts:
    104
    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.
     
    Griffo likes this.
  3. Ylisar

    Ylisar

    Joined:
    Jan 27, 2014
    Posts:
    19
    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.
     
  4. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    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
     
  5. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    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.
     
    jojoblaze likes this.
  6. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    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.
     
    FeastSC2 likes this.
  7. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    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:
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. // Put this on the topmost animation layer to get access to every animation played.
    5. // Listens to any animation entered in the state machine.
    6.  
    7. public class AnimatorListener : StateMachineBehaviour
    8. {
    9.     public int LastVisual { get; private set; }
    10.     public int CurrentVisual { get; private set; }
    11.  
    12.     /// <summary>
    13.     /// DO NOT REGISTER ON AWAKE, IT ONLY WORKS IN START DUE TO INHERITING FROM STATEMACHINEBEHAVIOUR
    14.     /// </summary>
    15.     public Action<Animator, AnimatorStateInfo, int> OnEnterState;
    16.     /// <summary>
    17.     /// DO NOT REGISTER ON AWAKE, IT ONLY WORKS IN START DUE TO INHERITING FROM STATEMACHINEBEHAVIOUR
    18.     /// </summary>
    19.     public Action<Animator, AnimatorStateInfo, int> OnUpdateState;
    20.     /// <summary>
    21.     /// DO NOT REGISTER ON AWAKE, IT ONLY WORKS IN START DUE TO INHERITING FROM STATEMACHINEBEHAVIOUR
    22.     /// </summary>
    23.     public Action<Animator, AnimatorStateInfo, int> OnExitState;
    24.  
    25.     void Awake()
    26.     {
    27.         LastVisual = -1;
    28.         CurrentVisual = -1;
    29.     }
    30.  
    31.     public sealed override void OnStateEnter(Animator _animator, AnimatorStateInfo _animatorStateInfo, int _layerIndex)
    32.     {
    33.         if (OnEnterState != null)
    34.         {
    35.             OnEnterState.Invoke(_animator, _animatorStateInfo, _layerIndex);
    36.         }
    37.  
    38.         LastVisual = CurrentVisual;
    39.         CurrentVisual = _animatorStateInfo.shortNameHash;
    40.     }
    41.  
    42.     public sealed override void OnStateUpdate(Animator _animator, AnimatorStateInfo _animatorStateInfo, int _layerIndex)
    43.     {
    44.         if (OnUpdateState != null)
    45.             OnUpdateState.Invoke(_animator, _animatorStateInfo, _layerIndex);
    46.     }
    47.  
    48.     public sealed override void OnStateExit(Animator _animator, AnimatorStateInfo _animatorStateInfo, int _layerIndex)
    49.     {
    50.         if (OnExitState != null)
    51.             OnExitState.Invoke(_animator, _animatorStateInfo, _layerIndex);
    52.     }
    53. }
    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?
     
    Last edited: Aug 7, 2018
  8. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    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:
    https://docs.unity3d.com/ScriptReference/SharedBetweenAnimatorsAttribute.html
    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.
     
    FeastSC2 likes this.
  9. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    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.
     
  10. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    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
     
    FeastSC2 likes this.
  11. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    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.

    upload_2018-8-14_1-17-55.png

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

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using Sirenix.OdinInspector;
    5. using UnityEditor;
    6. using UnityEngine;
    7. using Object = UnityEngine.Object;
    8.  
    9. [RequireComponent(typeof(UnitEvents))]
    10. [RequireComponent(typeof(Animator))]
    11. public class AnimatorEventBaker : MonoBehaviour
    12. {
    13.     // When can exit
    14.     // When can cancel/exit early
    15.     // When can flip direction based on input x
    16.     // When activate hitbox
    17.     // When deactivate hitbox
    18.  
    19.     // Question how to go back to idle properly and give ample time to make another strike.
    20.  
    21.     public UnitEvents UnitEvents;
    22.  
    23.     public static bool ShowName = true;
    24.  
    25.     public AnimState[] States;
    26.  
    27.     [Button("Get from Animator")]
    28.     private void GetEventsFromAnimator()
    29.     {
    30.         UnitEvents = GetComponent<UnitEvents>();
    31.         var clips = AnimationUtility.GetAnimationClips(gameObject);
    32.         var animator = GetComponent<Animator>();
    33.         var behaviours = animator.GetBehaviours<AnimatorEvent>();
    34.  
    35.         var states = new List<AnimState>();
    36.  
    37.         for (var i = 0; i < behaviours.Length; i++)
    38.         {
    39.             behaviours[i].UnitEvents = UnitEvents;
    40.             states.Add(behaviours[i].State);
    41.             Debug.Log("state found: " + behaviours[i].State.Name);
    42.             Debug.Log("State event: " + behaviours[i].UnitEvents);
    43.  
    44. //            EditorUtility.SetDirty(behaviours[i]);
    45.         }
    46.  
    47.         foreach (var c in clips)
    48.         {
    49.             EditorUtility.SetDirty(c);
    50.             Debug.Log("clip: " + c.name);
    51.         }
    52.  
    53.         AssetDatabase.SaveAssets();
    54.         AssetDatabase.Refresh();
    55.  
    56.         states = states.OrderBy(x => x.Name).ToList();
    57.  
    58.         States = states.ToArray();
    59.     }
    60.  
    61.     public void Bake()
    62.     {
    63.         GetEventsFromAnimator();
    64.         WriteEvents();
    65.     }
    66.  
    67.     [Button("Bake")]
    68.     private void WriteEvents()
    69.     {
    70.         // Get the clips
    71.         var clips = AnimationUtility.GetAnimationClips(gameObject);
    72.  
    73.         for (int i = 0; i < clips.Length; i++)
    74.         {
    75.             var currentClip = clips[i];
    76.  
    77.             foreach (var state in States)
    78.             {
    79.                 if (currentClip.name == state.Name)
    80.                 {
    81.                     var events = new List<AnimationEvent>();
    82.                     foreach (var ev in state.Events)
    83.                     {
    84.                         var target = ev.Target;
    85.                         if (target == null) target = state.BaseTarget;
    86.                         events.Add(CreateAnimationEvent(currentClip, target, ev.FunctionName, ev.NormalizedTime, ev.Int));
    87.                     }
    88.  
    89.                     foreach (var ev in state.AttackEvents)
    90.                     {
    91.                         var target = (GameObject)state.BaseTarget;
    92.                         var crap = target.GetComponent<Player>();
    93.                         events.Add(CreateAnimationEvent(currentClip, crap, ev.FunctionName.ToString(), ev.NormalizedTime, ev.Nr));
    94.                     }
    95.  
    96.                     AnimationUtility.SetAnimationEvents(currentClip, events.ToArray());
    97.                     EditorUtility.SetDirty(currentClip);
    98.                 }
    99.  
    100.             }
    101.         }
    102.  
    103.  
    104.         AssetDatabase.SaveAssets();
    105.         AssetDatabase.Refresh();
    106.  
    107.  
    108.         Debug.Log("Bake finished");
    109.     }
    110.  
    111.     [Button("Clear All")]
    112.     private void CleanAllAnimatorEvents()
    113.     {
    114.         // Get the clips
    115.         var clips = AnimationUtility.GetAnimationClips(gameObject);
    116.  
    117.         for (int i = 0; i < clips.Length; i++)
    118.         {
    119.             var currentClip = clips[i];
    120.  
    121.             CleanEventsFromClip(currentClip);
    122.         }
    123.     }
    124.  
    125.     private void CleanEventsFromClip(AnimationClip _clip)
    126.     {
    127.         AnimationUtility.SetAnimationEvents(_clip, new AnimationEvent[] { });
    128.     }
    129.  
    130.     private AnimationEvent CreateAnimationEvent(AnimationClip _clip, Object _object, string _functionName,
    131.         float _normalizedTime, int _int = 0)
    132.     {
    133.         float time2Trigger = _normalizedTime * _clip.length;
    134.         AnimationEvent anEvent = new AnimationEvent
    135.         {
    136.             functionName = _functionName,
    137.             objectReferenceParameter = _object,
    138.             time = time2Trigger,
    139.             intParameter = _int
    140.         };
    141.  
    142.         return anEvent;
    143.     }
    144. }
    145. #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?
     
  12. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    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
     
    FeastSC2 likes this.
  13. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    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?
     
  14. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    @FeastSC2 When you say at Editor time, do you mean during play mode in the editor or just in the editor not during playmode?
     
  15. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    I mean not during playmode, my goal is to preview my animations + fx in the scene view without starting playmode.
     
  16. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
  17. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    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]

    Code (CSharp):
    1.         Animator = GetComponent<Animator>();
    2.         var test1 = Animator.GetBehaviour<IdleBehaviour>();
    3.         if (test1 != null)
    4.         {
    5.             var context = AnimatorController.FindStateMachineBehaviourContext(test1);
    6.             AnimatorState state = context[0].animatorObject as UnityEditor.Animations.AnimatorState;
    7.             Debug.Log("state :" + state.name);
    8.         }
     
  18. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
  19. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
  20. Mecanim-Dev

    Mecanim-Dev

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    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

    Code (CSharp):
    1.  
    2. Animator = GetComponent<Animator>();
    3. var animatorController = Animator.runtimeAnimatorController as AnimatorController;
    4. var test1 = animatorController.GetBehaviour<IdleBehaviour>();
    5. if (test1 != null)
    6. {
    7.     var context = AnimatorController.FindStateMachineBehaviourContext(test1);
    8.     AnimatorState state = context[0].animatorObject as UnityEditor.Animations.AnimatorState;
    9.     Debug.Log("state :" + state.name);
    10. }
    11.  
     
    FeastSC2 likes this.
  21. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    I really needed to know which Animations are on the AnimationState the SMB is on, like I previously mentioned and thanks to FindStateMachineBehaviourContext it's possible. Thanks @Mecanim-Dev for that.

    Now that I have the animations in the SMB, I can pinpoint which one has a problem amongst many other valuable perks.

    However due to using an OnValidate() call my Unity can and will crash. I've seen that StateMachineBehaviours are not supposed to have an OnValidate(), however I haven't found any other reliable/easy way to have my SMB know which animations are on its animation state.

    What can I do to get those Animation clips without using OnValidate()?

    Code (CSharp):
    1. public class AnimatorSMB<TMonoBehaviour> : SealedSMB
    2.     where TMonoBehaviour : MonoBehaviour
    3. {
    4.     [HideInInspector] public AnimationClip AnimStateClip;
    5.     [HideInInspector] public AnimationClip[] BlendStateClips;
    6.     [HideInInspector] public bool IsBlendTree = false;
    7.     [HideInInspector] public string DebugLocationMessage;
    8.  
    9.     public virtual void OnValidate()
    10.     {
    11. #if UNITY_EDITOR
    12.         if (Application.isPlaying == false && !EditorApplication.isPlayingOrWillChangePlaymode &&
    13.             !EditorApplication.isCompiling && !EditorApplication.isUpdating)
    14.             GetEditorInfo();
    15. #endif
    16.     }
    17.  
    18. #if UNITY_EDITOR
    19.     public void GetEditorInfo()
    20.     {
    21.         var context = AnimatorController.FindStateMachineBehaviourContext(this);
    22.         if (context != null && context.Length > 0)
    23.         {
    24.             if (context[0] != null)
    25.             {
    26.                 var animatorController = context[0].animatorController;
    27.                 var animState = context[0].animatorObject as AnimatorState;
    28.                 if (animState != null)
    29.                 {
    30.                     var animClip = animState.motion as AnimationClip;
    31.                     var blendTree = animState.motion as BlendTree;
    32.                     var clipList = new List<AnimationClip>();
    33.                     if (blendTree != null)
    34.                     {
    35.                         foreach (var clip in blendTree.children)
    36.                         {
    37.                             var c = (AnimationClip)clip.motion;
    38.                             //                            Debug.Log("c: " + c.name);
    39.                             clipList.Add(c);
    40.                         }
    41.                     }
    42.                     IsBlendTree = animClip == null;
    43.                     if (IsBlendTree == false)
    44.                     {
    45.                         AnimStateClip = animClip;
    46.                         //                        Debug.Log("single c: " + animClip.name);
    47.                     }
    48.                     else BlendStateClips = clipList.ToArray();
    49.  
    50.                     if (animatorController != null)
    51.                     {
    52.                         DebugLocationMessage = "AnimatorSMB error: " + animatorController.name;
    53.                         if (IsBlendTree && blendTree != null)
    54.                         {
    55.                             DebugLocationMessage += "\nIsBlendTree: + " + IsBlendTree;
    56.                             DebugLocationMessage += "\nIsBlendTree: + " + blendTree.name;
    57.                         }
    58.                         else if (animClip != null)
    59.                         {
    60.                             DebugLocationMessage += "\animClip: + " + animClip.name;
    61.                         }
    62.                         var path = AssetDatabase.GetAssetPath(animatorController);
    63.                         if (path != string.Empty)
    64.                             DebugLocationMessage += "Path to controller: " + path;
    65.                     }
    66.                 }
    67.  
    68.             }
    69.         }
    70.     }
    71.  
    72. #endif
    73. }
     
  22. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
  23. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    I see that a StateMachineBehaviour changes it's InstanceID every time it's re-enabled.
    As shown in the screenshot below.

    Code (CSharp):
    1. public abstract class SealedSMB : StateMachineBehaviour
    2. {
    3.     private void OnEnable()
    4.     {
    5.         Debug.Log("ID: " + this.GetInstanceID());
    6.     }
    7. }
    upload_2019-3-8_14-23-27.png

    Not only does it loses its ID, it also loses the content of its variables when the Animator gets re-enabled.

    @JamesB That means your SceneLinkedSMB won't work when re-enabling an object because it will lose the reference to this: protected TMonoBehaviour m_MonoBehaviour;

    I also tested in the 2D Game Kit that uses the SceneLinkedSMB and it has the same problem. Disabling and re-enabling Ellen will cause it to make all the SceneLinkedSMBs malfunction.
     
    Last edited: Mar 8, 2019
  24. m-i-e-c-z

    m-i-e-c-z

    Joined:
    Nov 10, 2010
    Posts:
    27
    Hi Guys! I'm also using StateMachineBehaviours extensively in my projects and I came up with a custom approach for referencing scene objects. It works like this:

    1) Every object that has to be referenced from a StateMachineBehaviour has to have an UniqueObject.cs Monobehaviour attached or derive from it. This script is responsible to store the object's ID (game object's name - so it has to be unique) in a static UniqueObjectsManager - both in Editor and in Runtime.
    2) UniqueObjectsManager has static methods to build a Dictionary of all UniqueObjects in the scene - Build() is called from editor (via custom Editor scripts - Inspectors of StateMachineBehaviours). This allows me to have custom editors for StateMachineBehaviors that accept scene objects (with UniqueObject Monobehaviour attached). References are stored as strings in StateMachineBehaviors (I know, I know - strings are BAD, but it's more readable this way).
    3) I can get any UniqueObject from the UniqueObjectsManager by a static function Get(string uniqueID) - this way I can get an object I need in runtime within a StateMachineBehaviour and do whatever I need.

    You can find the UniqueObject.cs script with a static UniqueObjectManager attached. There is also an example of a StateMachineBehaviour script using an UniqueObject - StartMinigameBehaviour.cs. I've also attached a custom editor script for this StateMachineBehaviour to show how the unique ID is stored and retrieved in the Editor.

    I am not attaching the actual MinigameBase.cs script here - but it just needs to derive from UniqueObject.cs and have a public StartMinigame() method. I've attached a "dummy version" of the script.

    I know the system is not bullet proof, but you can basically build a quest editor around it or something similar. There are smarter ways to create IDs (you can use byte arrays or integers for example), but storing a name of a game object was good enough in my case.

    Hope it helps.
     

    Attached Files:

  25. m-i-e-c-z

    m-i-e-c-z

    Joined:
    Nov 10, 2010
    Posts:
    27
    Sorry for double posting - but I also have a question. Is there a way to override StateMachineTransition defaults in Unity Editor? Each time I create a transition between states in Animator Controller, I have to uncheck "Has Exit Time", set the transition duration to 0s and set a condition to the same trigger (It's always called "NEXT" in my case - and trigger it from the StateMachineBehaviour when it finished it's job).

    I know it is possible to create a preset for the transition, but I cannot force it to be the default preset for all newly created transitions in an Animator Controller.

    Using StateMachineBehaviours to drive some logic in a game is actually super powerful, but setting those transitions manually every time is quite annoying. I know I could use FlowCanvas or some other solution (I actually own them), but they are sometimes buggy, especially when a new Unity version is released

    Any ideas on how one could force different defaults for a state transition?
     
  26. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    I remember asking this question somewhere and I was told that changing the default won't be available for some time.
    They added the copy transition parameters for this.

    https://forum.unity.com/threads/mecanim-using-new-automatic-settings.494439/#post-3219195

    Does your method work when disabling and re-enabling the gameobject with the Animator?
    The system posted above loses its link when re-enabling the gameobject. See my post right above yours.
     
  27. m-i-e-c-z

    m-i-e-c-z

    Joined:
    Nov 10, 2010
    Posts:
    27
    Yes it works, because it gets the UniqueObject by it's ID in state's OnEnter() method. I store the UniqueObjects in a static manager. If you disable the object to which the Animator component is attached, it starts the graph over (from the default state) though. So I have my "flow manager" as I call it, always enabled. The object with the graph is always enabled. Having said that - you could easily work around that problem with restarting the animator component from it's default state. As states have unique names - you could store the current state name in a monobehviour and write a StateMachineBehaviour script in the default state to get the stored "current state" name and jump to it using animator.Play() or animator.CrossFade().
     
    FeastSC2 likes this.
  28. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    659
    Is extending StateMachineBehavior the only method to detect if a particular clip is finished playing, i.e. is there no method similar to Animator.IsClipPlaying(name)? Obviously, hacks involving elapsed time should be excluded.
     
  29. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    659
    Kybernetik and RecursiveFrog like this.
  30. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    350
    The bigger problem is that I’m not even sure that the StateMachineBehaviour even can tell you anything about the state of the underlying clips since there isn’t any guarantee that the start or end frame in the state has any bearing on the start or end frame of the source clip.

    It’s an artifact of having finite states that blend... it’s a contradiction in terms
     
  31. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    andreiagmu and stonstad like this.
  32. fwalker

    fwalker

    Joined:
    Feb 5, 2013
    Posts:
    255
    I am losing my references to the m_MonoBahaviour for the same reason as mentioned above (disabled objects that ger enabled during the game). Please tell me there is a solution for this!

    @JamesB That means your SceneLinkedSMB won't work when re-enabling an object because it will lose the reference to this: protected TMonoBehaviour m_MonoBehaviour;
     
  33. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    @fwalker The Initialise method does all the reference setup. Simply call that when your references are ready and put checks to not use null references in your SMBs.
     
  34. fwalker

    fwalker

    Joined:
    Feb 5, 2013
    Posts:
    255
    Little bit more help @JamesB pretty please.
    " The Initialise method" ==> what method exactly are you referring to?
     
  35. JamesB

    JamesB

    Unity Technologies

    Joined:
    Feb 21, 2012
    Posts:
    133
    @fwalker From my first code snippet in the initial post of this thread. SceneLinkedSMBs have a generic static method called Initialise which must be called to provide references to the scene.
     
  36. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    The "Initialise" method can only be called in the Start() method, if you call it in Awake or OnEnable directly it won't work.

    Like JamesB mentioned, you can get the references again by calling the Initialise method but you can't call it in OnEnable for some reason... It would be interesting to know why.

    Anyway, if you make sure the first time Initialise is called is in the Start method => You can later re-use it in OnEnable afterwards, this fixed the problem for me.


    Code (CSharp):
    1.     private bool IsInitialized;
    2.     void Start()
    3.     {
    4.         if (IsInitialized == false)
    5.         {
    6.             IsInitialized = true;
    7.             AnimatorSMB<AnimatorEventManager>.InitialiseInStart(Animator, this);
    8.         }
    9.     }
    10.  
    11.     void OnEnable()
    12.     {
    13.         // Important: We call this because when this component gets disabled the SMB loses the reference to the MonoBehaviour.
    14.         if (IsInitialized)
    15.             AnimatorSMB<AnimatorEventManager>.InitialiseInStart(Animator, this);
    16.     }
    17.  
     
  37. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978

    I was wrong, it doesn't work in OnEnable() ;(
    I'll try and find another way. Initialise cannot be called in OnEnabled.
     
  38. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    Strange you're saying Initialise doesn't work in OnEnable, I started experimenting with this script yesterday and it works perfectly fine for me. I'm using Unity 2019.3.5 for this. I can disable and enable my objects just fine, they restore their references no problem.
     
    FeastSC2 likes this.
  39. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    You're right, I later realized I was incorrect.
     
  40. sarynth

    sarynth

    Joined:
    May 16, 2017
    Posts:
    98
    Lo and behold. 2 years after the thread started in 2017.

    https://github.com/Unity-Technologi...3DGamekit/Scripts/Game/Core/SceneLinkedSMB.cs

    While I'm not using this yet to its full potential, since I'm not taking advantage of the specific callbacks for the various stages of the transitions and clips -- I do really appreciate the concept and the approach. Extending StateMachineBehaviour, then initializing them all from a main script on the monobehavior to set up references is smart thinking.

    Thanks for sharing.
     
    JayFiraja and stonstad like this.
  41. fwalker

    fwalker

    Joined:
    Feb 5, 2013
    Posts:
    255
    Are we still messing around with all this for 2019-20 ?
    Should would be nice to have it built-in.
     
  42. TurboNic

    TurboNic

    Joined:
    Oct 7, 2015
    Posts:
    5
    Love this system, and I've been using it a lot! I did just discover, however, that the SceneLinkedSMB will lose its MonoBehaviour reference if the object is disabled.

    I just added
    animator.keepAnimatorControllerStateOnDisable = true;
    in SceneLinkedSMB.Initialise and that seems to fix the issue, in case anyone else is having that problem.
     
    Pleija and acornellier like this.
  43. fodel

    fodel

    Joined:
    May 5, 2021
    Posts:
    4
    Animator.GetBehaviour renvoie l'instance d'exécution du SMB, de sorte que la raison pour laquelle ils ne correspondent pas. Vous devez obtenir le SMB du contrôleur car vous recherchez l'actif SMB et non l'instance d'exécution.








    snaptube vidmate
     
    Last edited: May 6, 2021
  44. mysleepyhead

    mysleepyhead

    Joined:
    Nov 26, 2021
    Posts:
    1
  45. Ylisar

    Ylisar

    Joined:
    Jan 27, 2014
    Posts:
    19
    Is there any plans to change how StateMachineBehaviours works in this regard? SceneLinkedSMB which has been linked here is merely a band aid & still doesn't really solve everything. Like OnSLStatePreExit not guaranteed to be called makes it a pretty useless function. We need to have something along the lines of:

    OnTransitionToBegin() // This is essentially the current OnStateEnter, we've just started to transition to our state, should be guaranteed to be called before the first OnStateUpdate()
    OnTransitionToEnd() // Called when our state has finished being transitioned to.
    OnStateUpdate() // Called for every frame when we're actually updated, this should include both when we're being transitioned to AND when we're being transitioned from, preferably AnimatorStateInfo contains an enum which states whether we're being transition to, no transition in flight, or we're being transitioned from
    OnTransitionFromBegin() // Called when we begin transitioning out of our state
    OnTransitionFromEnd() // Called when our state has completed transition, should ideally be guaranteed that this is called directly after the last OnStateUpdate()

    Considering states A -> B -> C with us currently being inside A:
    A.OnTransitionFromBegin();
    B.OnTransitionToBegin();
    While transition is going on A.OnStateUpdate() & B.OnStateUpdate() preferably with enough context to know that we're in a transition & whether we're leaving or entering
    B.OnTransitionToEnd();
    A.OnTransitionFromEnd();

    B.OnStateUpdate(); while in state

    B.OnTransitionFromBegin();
    C.OnTransitionToBegin();
    ...

    Ideally the transition related functions should contain an argument of a new(?) surely already existing data type, the actual SM state. This one then has at least state.GetBehaviours<>(), which would open up a whole lot of more options on how to design StateMachineBehaviours.
     
    andreiagmu likes this.
  46. LeFx_Tom

    LeFx_Tom

    Joined:
    Jan 18, 2013
    Posts:
    88
    It would be so nice, if Mecanim/Animator would see some love like this or in general better accessible and stable ways to react to transistions/states.
    As Animator seems to be the go-to-way to do animations for at least another year or two (see roadmap for DOTS), it should have seen work/improvements since months (instead of letting it wither aways as it has been the case)

    I don't really understand, why it has to be so clunky to create reliable ways to figure out state-actions....especially since the animator is Unitys singular integrated visual state-tree. You can use it for a lot of things aside from character animations, but it can hurt you badly to do so and to rely on it.

    I would love, if non-destructive ways to create events/callbacks would be introduced. Relying on animation-events is problematic, because they can get lost after reimport/change on source-animations or by fixed-time-states, that just don't reach that point in time.

    @Unity - please consider putting a bit more effort in a system, on which we will have to rely for at least 2 more years (with given LTS lifecycles) - please!
     
    andreiagmu likes this.
  47. kinorotpirsum

    kinorotpirsum

    Joined:
    Dec 14, 2022
    Posts:
    10
    StateMachineBehaviour
    Description
    StateMachineBehaviour is a component that can be added to a state machine state. It's the base class every script on a state derives from.

    By default the Animator does instantiate a new instance of each behaviour define in the controller. The class attribute SharedBetweenAnimatorsAttribute controls how behaviours are instantiated.

    StateMachineBehaviour has some predefined messages: OnStateEnter, OnStateExit, OnStateIK, OnStateMove, OnStateUpdate.

    using UnityEngine;

    public class AttackBehaviour : StateMachineBehaviour
    {
    public GameObject particle;
    public float radius;
    public float power;

    protected GameObject clone;

    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    clone = Instantiate(particle, animator.rootPosition, Quaternion.identity) as GameObject;
    Rigidbody rb = clone.GetComponent<Rigidbody>();
    rb.AddExplosionForce(power, animator.rootPosition, radius, 3.0f);
    }

    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    Destroy(clone);
    }

    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    Debug.Log("On Attack Update ");
    }

    override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    Debug.Log("On Attack Move ");
    }

    override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    Debug.Log("On Attack IK ");
    }
    }
     
    Last edited: Dec 29, 2022
  48. markshandor22

    markshandor22

    Joined:
    Dec 14, 2022
    Posts:
    1
    Are you a programmer by profession?
     
  49. kinorotpirsum

    kinorotpirsum

    Joined:
    Dec 14, 2022
    Posts:
    10
    Yes and also a QA specialist
     
  50. scoutodin11

    scoutodin11

    Joined:
    Jan 13, 2023
    Posts:
    1
    I try this in my work..
     
Thread Status:
Not open for further replies.