Search Unity

Randomize animations using parameters

Discussion in 'Animation' started by Cileb, Mar 2, 2019.

  1. Cileb

    Cileb

    Joined:
    Feb 20, 2018
    Posts:
    6
    I have a player character who can attack, charge attack, and dash. The attack and dash states each have 2-3 animations that slightly differ from one another. One attack animation for example is left-handed and another right-handed. At the moment I am using sub-state machines for transitioning between idle, attack, charge attack, and dash.

    I want a different animation to play every time the character attacks or dashes. I have a somewhat working solution:

    • I'm using bool conditions for the transitions between states, such as isAttacking, isCharging, and isDashing.
    • Animations within these sub-states use int conditions. My left-handed attack has a condition of randomAttack = 0 and right-handed attack has a condition of randomAttack = 1. I call Random.Range in my player controller script to set this integer on Input.GetButton.
    But it's not quite doing what I want. Here's my problem:
    • Using bool conditions between states allows button holds to loop states: holding down the attack button will play attack for the whole press.
    • My attack and dash animations are randomized each time the button is pressed but not while the button is held down. Three presses might play left--right--left attack but a long press will only play left--left--left, or right--right--right. I want it randomized on both press and hold.
    I'm not sure what I'm doing wrong. Here's some of my code:

    Code (CSharp):
    1. void Update()
    2.     {
    3.         ...
    4.         Attack();
    5.         Charge();
    6.         Dash();
    7.     }
    8.  
    9. void Attack()
    10.     {
    11.         if (player.GetButtonDown("Fire1"))
    12.         {
    13.             anim.SetBool("isCharging", false);
    14.             anim.SetBool("isDashing", false);
    15.         }
    16.         else if (player.GetButton("Fire1"))
    17.         {
    18.             anim.SetBool("isAttacking", true);
    19.             anim.SetInteger("randomAttack", Random.Range(0, 2));  // left=0, right=1
    20.         }
    21.         else if (player.GetButtonUp("Fire1"))
    22.         {
    23.             anim.SetBool("isAttacking", false);
    24.         }
    25.     }
     
  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Your code looks like it should essentially do what you want (though isAttacking might work better as a trigger that you only set once in GetButtonDown) so the source of the problem is likely somewhere in your AnimatorController. That's actually one of the main problems with Mecanim: you waste a lot of time setting up the same logic in both the AnimatorController and your code, and then whenever you need to track down a bug you often waste even more time fighting out which one is the source of the problem and trying to fix it without breaking anything else.

    This problem could be solved very easily using my Animancer plugin (link in my signature below). Instead of setting up an AnimatorController and then messing around with parameter and transition settings, it lets you directly play the AnimationClip you want. So to play a random attack you could just give your script an array of AnimationClips and pick a random one to play each time you attack.

    Here's what your script might look like if you used Animancer:

    Code (CSharp):
    1. [SerializeField]
    2. private AnimancerController _Animancer;
    3.  
    4. [SerializeField]
    5. private AnimationClip[] _AttackAnimations;
    6.  
    7. void Update()
    8. {
    9.     ...
    10.     Attack();
    11.     Charge();
    12.     Dash();
    13. }
    14.  
    15. void Attack()
    16. {
    17.     if (player.GetButtonDown("Fire1"))
    18.     {
    19.         _Animancer.CrossFade(_AttackAnimations[Random.Range(0, _AttackAnimations.Length)]);
    20.     }
    21. }
    Less code, no hard coded magic strings to identify parameters, and no need to waste time making sure your scripts play nice with your AnimatorControllers.

    Edit: that CrossFade would likely be better as CrossFadeNew since it might try to fade from an attack animation into the same attack animation. Or if you're using 2D sprites you probably just want to use Play without any fading.
     
  3. Cileb

    Cileb

    Joined:
    Feb 20, 2018
    Posts:
    6
    Thanks for the recommendation. I was eyeing Animancer but decided to try the ins and outs of Mecanim first. And this is embarrassing but I found out why my code wasn't doing what I wanted. In case anyone else runs into this, here's what I learned.

    My Attack function is called in Update and so the Random.Range under GetButton also ran every frame. For some odd reason, the same number kept being randomized every frame my animations looped. I didn't need Random.Range to run every frame, only once every other second or however long between loops.

    I did this with a coroutine:

    Code (CSharp):
    1. void Attack()
    2. {
    3.     if (player.GetButtonDown("Fire1"))
    4.     {
    5.         anim.SetBool("isCharging", false);
    6.         anim.SetBool("isDashing", false);
    7.         StartCoroutine("RandomAttack");
    8.     }
    9.     else if (player.GetButtonUp("Fire1"))
    10.     {
    11.         anim.SetBool("isAttacking", false);
    12.     }
    13. }
    14.  
    15. IEnumerator RandomAttack()
    16. {
    17.     while (player.GetButton("Fire1"))
    18.     {
    19.         anim.SetBool("isAttacking", true);
    20.         anim.SetInteger("randomAttack", Random.Range(0, 2));
    21.         yield return new WaitForSeconds(.25f);     // waits for .25 seconds before repeat
    22.     }
    23.     yield return null;
    24. }
    The parameters now randomize on button press and hold.