Controlling animation blending through code (without state machines/mecanim)

Let’s say I want to ditch state machines, and control entirety of animation for a character through code. Meaning handling blending of animation clips on my own.

How would I need to go about it?

As far as I can tell, only legacy animation system provides anything like this, but mecanim pretty much requires state machines.

Am I missing anything?

Yes. Totally doable. Here some tests i make with success.

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// MOON(S) 20.62 MODULE ANIMATION +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// -n$(Line) "$(File)"
#pragma warning disable 0168 // variable declared but not used.
#pragma warning disable 0414 // is assigned but its value is never used
#pragma warning disable 0642 // Possible mistaken empty statement
using UnityEngine;
using System;
using System.Collections;


public class RobotAnimTEST : MonoBehaviour{
   private int myID;
   private Transform PiedGauche;
   private Transform PiedDroit;
   const float CrossFadeTempo = float.NegativeInfinity; // IMPORTANT !!!

   protected Animator animRobot;

   private Rigidbody rigBody;
   private float Speed = 0.0f;
   private   float Pg = 0.0f;
   private   float Pd = 0.0f;

   private AnimatorStateInfo currentState;
   // Animation flags / Var ------------------------------------------------------------------------------
   private bool IsArmed  = false;
   private bool IsWeapon = false;
   private bool IsWalk  = true;
   private bool IsRun  = false;
   private bool IsCrouch = false;
   private bool IsCover  = false;
   private bool ICanStartMove = true;
   private bool ICanStopMove  = false;
   private bool ICanBlendMove = false;
   private bool ImInCrouch  = false;
   private bool ImInBlend  = false;
   private bool ImInIdle  = false;
   private int WeaponLayer = 1;
   private float WeaponLayerWeight = 0.0f;
   private float GetWeaponTime  = 0.0f; // Time to arm the weapon.
   private float DropWeaponTime  = 0.0f; // Time to drop the weapon.

   private float IdlePlayTime  = 0.0f;

   private float RetardAnimation = 0.0f;
   private string AnmName  = "";  // Nom Anim
   private float  AnmDuree  = 0.0f; // Duree Anim. Pour le retard d'Invoke
   private float  AnmSpeed  = 0.0f; // Speed du Perso
   private float  AnmSpeedDelais = 0.0f; // Retard au demarrage du perso. Eviter effet de "glissement"
   private float  AnmStopDelais  = 0.0f; // Retard a l'arret du perso. Eviter effet de "glissement"
   // Animation flags ------------------------------------------------------------------------------------
   [Header("TWEAK DU TEMPO CROUCH")]
   [Range(0.05f,0.08f)] public float TimeCrouchToWalk  = 0.05f;
   [Range(0.05f,0.08f)] public float TimeCrouchToRun  = 0.05f;
   [Range(0.02f,0.05f)] public float TimeStandToCrouch = 0.05f;
   [Range(0.30f,0.60f)] public float TimeToCrouchIdle  = 0.50f;
   [Range(0.01f,0.04f)] public float TimeCrouchToStand = 0.03f;
   [Range(0.20f,0.30f)] public float TimeToBlend  = 0.30f;

// *******************************************************************************
// *******************************************************************************
/*
   Nom,Duree,Speed,RetardVitesse,StopVitesse
1   IDLE1,0,0,0,0
2   IDLE2,0,0,0,0
3   IDLE3,0,0,0,0
4   IDLE4,0,0,0,0
5   IDLE5,0,0,0,0
6   IDLE6,0,0,0,0
7   WALK,0,0,0,0
8   RUN,0,0,0,0
9   CROUCH,0,0,0,0
10   WALKSTART,0.767,1,0.1,0
11   RUNSTART,0.767,3.2,0.2,0
12   CROUCHSTART,0.6,1,0,0
13   STOP_WALK_GAUCHE,0,0,0,0.6
14   STOP_WALK_DROIT,0,0,0,0.6
15   STOP_RUN_GAUCHE,0,0,0,0.4
16   STOP_RUN_DROIT,0,0,0,0.4
17   STOP_CROUCH_GAUCHE,0,0,0,0
18   STOP_CROUCH_DROIT,0,0,0,0
19   RIFLE_WALKSTART,0.767,1,0.1,0
20   RIFLE_WALK,0,0,0,0
21   RIFLE_STOP_WALK_GAUCHE,0,0,0,0.6
22   RIFLE_STOP_WALK_DROIT,0,0,0,0.6
23   IDLECROUCH,0,0,0,0
24   GETGUN,0,0,0,0
25   DROPGUN,0,0,0,0
*/
// *******************************************************************************
// *******************************************************************************

//   public readonly string[] names = { "alpha", "beta", "gamma", "delta" };
   public  string[] names = { "alpha", "beta", "gamma", "delta" };

   public void NewEvent() // test event d'animation.
   {
     Debug.Log("EVENT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
   }

   void Start (){
     myID = GetInstanceID();
     animRobot = GetComponent<Animator>();
     animRobot.applyRootMotion = false;
     //animRobot.cullingMode = AnimatorCullingMode.CullUpdateTransforms; // Retarget, IK and write of Transforms are disabled when renderers are not visible.
     //animRobot.cullingMode = AnimatorCullingMode.AlwaysAnimate;  // Always animate the entire character. Object is animated even when offscreen.
     animRobot.cullingMode = AnimatorCullingMode.CullCompletely;  // Animation is completely disabled when renderers are not visible.
     //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     //animRobot.updateMode = AnimatorUpdateMode.Normal;  // Normal update of the animator.
     animRobot.updateMode = AnimatorUpdateMode.AnimatePhysics; // Updates the animator during the physic loop in order to have the animation system synchronized with the physics engine.
     //animRobot.updateMode = AnimatorUpdateMode.UnscaledTime; // Animator updates independently of Time.timeScale.

     rigBody = GetComponent<Rigidbody>();

     PiedGauche = animRobot.GetBoneTransform(HumanBodyBones.LeftFoot);
     PiedDroit  = animRobot.GetBoneTransform(HumanBodyBones.RightFoot);

     ICanStartMove = true;
     ICanStopMove  = false;
     ICanBlendMove = false;

     WeaponLayerWeight = 0.0f; // IMPORTANT ?!
     animRobot.SetLayerWeight(WeaponLayer, WeaponLayerWeight);

     IdleMove();
   }

   void Update(){
   
     if (Input.GetKeyDown("m") && ICanStartMove) InitMove();
     if (Input.GetKeyDown("m") && ICanBlendMove) InitBlend();
     if (Input.GetKeyDown("s") && ICanStopMove)  StopMove();
     if (Input.GetKeyDown("w")) GetWeapon();
     if (Input.GetKeyDown("c")) DropWeapon();
     //bool isRunningPressed = Input.GetKey("left shift") && Input.GetKey("w");

     IsWalk  = true;
     IsCrouch = false;
     IsRun  = false;

     if (Input.GetKey(KeyCode.LeftShift)) // Run ++++++++++++++++++++
     {
       IsWalk  = false;
       IsCrouch = false;
       IsRun  = true;
     }
     if (Input.GetKey(KeyCode.RightShift)) // Crouch ++++++++++++++++
     {
       IsWalk  = false;
       IsCrouch = true;
       IsRun  = false;
     }
   }

   void FixedUpdate(){
     if (ImInIdle){
       //float playbackTime = animRobot.GetCurrentAnimatorStateInfo(0).normalizedTime; // Entier = le nombre de fois que l'anim a été jouée. Décimal = % de play de l'anim
       // NE FONCTIONNE PAS EN CROSFADE !!!!!!!!!!!
       //playbackTime = (float)Math.Round((float)playbackTime, 2);
       //Debug.Log("playbackTime = " + playbackTime);
       //Debug.Log("IsInTransition =  "  + animRobot.IsInTransition(0)); // ne semble fonctionner que pour le CrossFade !!!
       if (Time.time > IdlePlayTime){
         IdleMove();
       }
     } else rigBody.MovePosition(transform.position + transform.forward * Time.fixedDeltaTime * Speed );

     //if (!ImInIdle) rigBody.MovePosition(transform.position + transform.forward * Time.fixedDeltaTime * Speed );

     // Get Weapon and close Weapon *********************************************************
     if (IsWeapon && WeaponLayerWeight < 1.0f){
       WeaponLayerWeight += Time.fixedDeltaTime / GetWeaponTime;
       animRobot.SetLayerWeight(WeaponLayer, WeaponLayerWeight);
     }
     if (!IsWeapon && WeaponLayerWeight > 0.0f){
       WeaponLayerWeight -= Time.fixedDeltaTime / DropWeaponTime;
       animRobot.SetLayerWeight(WeaponLayer, WeaponLayerWeight);
     }
     // Get Weapon and close Weapon *********************************************************

   }

   // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
   // GESTION ANIM ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

   void InitMove() // ============================================================================
   {
     ImInIdle = false;
     if (IsArmed){
       // Anim mode Armed
       if (IsWalk){
         AnmName  = "RIFLE_WALKSTART";
         AnmDuree  = 0.767f;
         AnmSpeed  = 1.0f;
         AnmSpeedDelais = 0.10f;
       }
       if (IsRun);
       if (IsCrouch);
     } else
     {
       if (IsWalk){
         AnmName  = "WALKSTART";
         AnmDuree  = 0.767f;
         AnmSpeed  = 1.0f;
         AnmSpeedDelais = 0.10f;
       }
       if (IsRun){
         AnmName  = "RUNSTART";
         AnmDuree  = 0.767f;
         AnmSpeed  = 3.2f;
         AnmSpeedDelais = 0.2f;
       }
       if (IsCrouch){
         AnmName  = "CROUCHSTART";
         AnmDuree  = 0.600f;
         AnmSpeed  = 1.0f;
         AnmSpeedDelais = 0.0f;
       }
     }

     // Y'a du crouch ici !!!!!!! ******************************
     if (ImInCrouch && IsWalk){
       ImInCrouch = false;
       animRobot.CrossFade( "WALK", TimeCrouchToWalk, -1, CrossFadeTempo);
       Invoke("InitSpeed", AnmSpeedDelais);
     } else if (ImInCrouch && IsRun) // CROUCH A DEBOUT!!!
     {
       ImInCrouch = false;
       animRobot.CrossFade( "RUN", TimeCrouchToRun, -1, CrossFadeTempo);
       Invoke("InitSpeed", AnmSpeedDelais);
     } else if ( !ImInCrouch && IsCrouch)
     {
       ImInCrouch = true;
       animRobot.CrossFade( "CROUCH", TimeStandToCrouch, -1, CrossFadeTempo);
       Invoke("InitSpeed", AnmSpeedDelais);
     } else StartMove();
     // Y'a du crouch ici !!!!!!! ******************************

     //******************************************************************************
     //AnimatorStateInfo animationState  = animRobot.GetCurrentAnimatorStateInfo(0);
     //AnimatorClipInfo[] myAnimatorClip = animRobot.GetCurrentAnimatorClipInfo(0);
     //float myTime = myAnimatorClip[0].clip.length;
     //Debug.Log("Retard = " + RetardAnimation + " / Nouveau = " + myTime);
     //******************************************************************************
   }

   void StartMove(){
     animRobot.Play(AnmName);

     RetardAnimation = AnmDuree; // retard de la prochaine animation (LOOP)
     Invoke("LoopMove", RetardAnimation);
     Invoke("InitSpeed", AnmSpeedDelais); // Declencher le retard de mouvement duu Perso
   }

   void InitBlend(){
     ImInBlend = true;
     LoopMove();
   }

   void LoopMove() // =============================================================================
   {
     if (IsArmed){
       // Anim mode Armed
       if (IsWalk) AnmName = "RIFLE_WALK";;
       if (IsRun);
       if (IsCrouch);
     } else
     {
       if (IsWalk)    AnmName = "WALK";
       if (IsRun)  AnmName = "RUN";
       if (IsCrouch) AnmName = "CROUCH";
     }
     if (ImInBlend){   
       if (IsRun) StopMove();
       else{
         Pg = PiedGauche.position.y; // y
         Pd = PiedDroit.position.y;  // y
//       Debug.Log("> ***Pied Gauche = " + Pg);
//       Debug.Log("> ***Pied Droit  = " + Pd);
       }

       float delta = Math.Abs(Pg - Pd);
       //if(Pg > Pd) Debug.Log("******************Pied Gauche plus haut de = " + (float)(Pg - Pd)); else Debug.Log("******************Pied Droit plus haut de = " + (float)(Pd - Pg));

       if (Pg > Pd) animRobot.CrossFade(AnmName, TimeToBlend, -1, 0.41f + delta); // Debuter l'anim au bon pied en l'air !!!
       else animRobot.CrossFade(AnmName, TimeToBlend, -1, CrossFadeTempo);
       ImInBlend = false;
     } else animRobot.Play(AnmName);
   }

   void StopMove() // =============================================================================
   {
     //Transform toto = animRobot.GetBoneTransform(HumanBodyBones.LeftFoot);
     //Debug.Log(Pg + " Pied " + toto.position );
     Pg = PiedGauche.position.y;
     Pd = PiedDroit.position.y;

     if (IsArmed){
       // Anim mode Armed
       if (IsWalk){
         AnmStopDelais = 0.6f;
         if (Pg > Pd) animRobot.CrossFade("RIFLE_STOP_WALK_GAUCHE", 1.0f, -1, CrossFadeTempo);
         else  animRobot.CrossFade("RIFLE_STOP_WALK_DROIT",  1.0f, -1, CrossFadeTempo);
       }
       if (IsRun);
       if (IsCrouch);
     } else
     {  // Anim mode "Civil"
       if (IsWalk){
         AnmStopDelais = 0.6f;
         if (Pg > Pd) animRobot.CrossFade("STOP_WALK_GAUCHE", 1.0f, -1, CrossFadeTempo);
         else  animRobot.CrossFade("STOP_WALK_DROIT",  1.0f, -1, CrossFadeTempo);
       }
       if(IsRun){
         AnmStopDelais = 0.4f;
         if (Pg > Pd) animRobot.CrossFade("STOP_RUN_GAUCHE", 1.0f, -1, CrossFadeTempo);
         else  animRobot.CrossFade("STOP_RUN_DROIT",  1.0f, -1, CrossFadeTempo);
       }
       if (IsCrouch){
         AnmStopDelais = 0.0f;
         if (Pg > Pd) animRobot.CrossFade("STOP_CROUCH_GAUCHE", TimeToCrouchIdle, -1, CrossFadeTempo); // mettre variable ici !!!!!!!!!!!!!!!!!!!!!!!
         else  animRobot.CrossFade("STOP_CROUCH_DROIT",  TimeToCrouchIdle, -1, CrossFadeTempo); // mettre variable ici !!!!!!!!!!!!!!!!!!!!!!!
       }
     }
     if (!ImInBlend){
       Invoke("StopVitesse", AnmStopDelais); // DELAI A VOIR !!!
       Invoke("IdleMove",  0.6950f); // DELAI A VOIR !!!
     }
   }

   void IdleMove() // ==============================================================================
   {
     ICanStartMove = false;
     ICanStopMove  = false;
     ICanBlendMove = false;
     ImInCrouch  = false;
     ImInIdle  = false;

     if (IsArmed){
       // Anim mode Armed
       if (IsWalk);
       if (IsRun);
       if (IsCrouch);
     } else
     {
       if (IsWalk || IsRun ){
         ImInIdle = true;
         UnityEngine.Random.seed = (int)System.DateTime.Now.Millisecond + myID;
         IdlePlayTime = Time.time + UnityEngine.Random.Range(8.0f,15.0f); // A VOIR !!!!
//         Debug.Log("Time " + Time.time + "  /  IdlePlayTime = " + IdlePlayTime);
         int idleRandom = UnityEngine.Random.Range(1,6);
         switch (idleRandom){
           case 1:
             AnmName = "IDLE1";
             break;
           case 2:
             AnmName = "IDLE2";
             break;
           case 3:
             AnmName = "IDLE3";
             break;
           case 4:
             AnmName = "IDLE4";
             break;
           case 5:
             AnmName = "IDLE5";
             break;
           case 6:
             AnmName = "IDLE6";
             break;
         }
       }
       if (IsCrouch){
         AnmName = "IDLECROUCH";
         ImInCrouch = true;
       }
     }
     animRobot.CrossFade(AnmName, 0.25f, -1, -1); // 0.25f A VOIR !!!
     ICanStartMove = true;
   }

   void InitSpeed() // ===========================================================================
   {
     Speed = AnmSpeed;
     ICanStartMove = false; // DeActiver Move
     ICanStopMove  = true;  // Activer Stop
     ICanBlendMove = true;
   }

   void StopVitesse() // ===========================================================================
   {
     Speed = 0.0f;
   }

   void GetWeapon(){
     IsWeapon = true;
     WeaponLayerWeight = 0.0f;
     UnityEngine.Random.seed = (int)System.DateTime.Now.Millisecond;
     GetWeaponTime  = UnityEngine.Random.Range(0.5f, 0.8f); //
     DropWeaponTime = UnityEngine.Random.Range(2.7f, 3.0f); //
     animRobot.CrossFade( "GETGUN", 0.1f, -1, CrossFadeTempo);
     //animRobot.speed = 0.5f;
     //Debug.Log("DropWeaponTime......" + DropWeaponTime);
   }

   void DropWeapon(){
     IsWeapon = false;
     animRobot.CrossFade( "DROPGUN", 0.1f, -1, CrossFadeTempo);
   }
   // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
   // GESTION ANIM +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// MOON(S) 20.62 MODULE ANIMATION +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 Likes

Oh, wow. Thanks.

So, it is mostly centered around CrossFade function, hmm.

Is it your code, by the way? Just curious.

Yes. A work base not necessarily optimized, but effective. You can use it if you want.:wink:

Alright, thanks.

It would take me quite a bit of time to figure out that CrossFade can be performed between two random nodes in animator.

I think It will work well for what i had in mind. Basically, I wanted something similar to unreal montages, or saner way to handle complex combat animations with multiple states without having to recreate transitions for every new character.

Either way, thanks for the info ^_^.

1 Like

@TonyLi may have some additional info to add. He suggests and has discussed crossfade at length in numerous prior posts.

Nope, ZJP covered it well.

The only thing I’d add is that it can sometimes simplify things a lot to mix and match. There are some sequences of animations, like [pre-jump]–>[jump]–>[land] that would be more complicated in code than visual transitions. CrossFade to [pre-jump], then CrossFade again after [land]. But let Mecanim handle the tedious sequential transitions in between.

2 Likes