Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

2D Animation Layer Sync, help with weights

Discussion in 'Animation' started by ScratchModed, Jul 29, 2015.

  1. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Howdy, Howdy, Howdy.

    I think I've become error blind in my coding, but I need some help getting my layer syncing to work better. I'm currently working on a 2D platformer with sprites (think Megaman). I've managed to create two sync animator layers for the characters two states of Base and Attacking.

    The trouble I run into is how to switch back and forth between the states immediately. Currently the way I am doing it with a co-routine; This works fine if I wait for the co-routine to finish between attacks, but it seems like if I mash the attack button (keep thinking Megaman) the attack layer weight will start to flicker on and off as if the co-routine is queuing itself and not caring if I re-enter the attack state.

    So how do I make my Endshoot co-routine abort or start over if I keep re-entering the attack state?
    Code (CSharp):
    1. void Shoot () {
    2.         GameObject obj = ObjectPooler.current.GetPooledObject ();
    3.         if (obj == null) return;
    4.         obj.transform.position = firePoint.transform.position;
    5.         obj.transform.rotation = firePoint.transform.rotation;
    6.         obj.SetActive(true);
    7. //Setting Attack Sync layer to viewable and then turning it off (Layer#, Weight)
    8.         anim.SetLayerWeight (1,1);
    9.         StartCoroutine (EndShoot ());
    10.     }
    11.     IEnumerator  EndShoot(){
    12.         yield return new WaitForSeconds (0.4f);
    13.         anim.SetLayerWeight (1,0);
    14.  
    15.     }
    16. //END Setting attack Sync Layer to viewable.
     
  2. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    Rather than using coroutine, you should use a StateMachineBehaviour and override OnStateExit()

    On your state shoot on your synchronized layer add a StateMachineBehaviour, override the function OnStateExit which is called when your animation is finish and simply set your layer weight to 0.

    http://docs.unity3d.com/ScriptReference/StateMachineBehaviour.html
     
  3. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    So if that looks something like this
    Code (CSharp):
    1.  
    2.  
    3. public class AttackEnd : StateMachineBehaviour {
    4.  
    5.  
    6.     override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    7.     {
    8.         animator.SetLayerWeight(1,0);
    9.     }
    then I get a scenario where if I am running , the animation loops instead of triggering a transition for OnStateExit to see, in which case my character will be stuck in attack mode until I perform an transition-able action.
    Is there a way that at the end of the animation's first loop it can switch the layerweight to 0?
     
  4. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    There is many way to do this.

    You could override StateMachineBehaviour.OnStateUpdate() and trigger a transition from attack to idle when normalize time is equal 1, each time a clip loop the normalize time interger part is increased by one.
    Code (CSharp):
    1.    
    2. override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    3.         if (stateInfo.normalizedTime > 1.0f)
    4.             animator.CrossFade("Idle", 0.1f);
    5.     }
    6.  
    7.     // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    8.     override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    9.         animator.SetLayerWeight(1, 0);
    10.     }
    11.  
    Or simply create a transition from attack to idle with exit time equal 1 and use the same OnStateExit() function to lower the layer weight to 0.
     
  5. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    I imported MegaMan for the sake of making this more viewable:

    Resetting to Idle in the middle of a run animation creates a stutter step even if all transition durations have no exit time and are set to 0.


    Perhaps I should look at this a different way, Is there a way to have the layer weights change in the middle of the animations instead of the end and without requiring a animator transition to happen?
     
  6. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    You have complete control over layer weight from script so you can do what you want,It the only way to change a layer weight.

    I'm not sure what you mean by stutter step, from the giff it look like like megaman is sliding back a few step, this is what you mean?
     
  7. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Nope that's just the end of gif. If you watch you can see megaman's Left hand appear behind his back every time he swings his left leg forward. If slowed down during that stutter it looks like this .
    What you're seeing is it switching from Run n Gun> Idle n gun>Running without gun>Run n gun.
    So it not only switches to 1 frame of idle on not the base layer ,but also three frames of running(base) before it figures out to switch back to Run n Gun.
     
  8. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    So this seem related to how you did build your state machine.

    If you can share your project, I could take a look at your statemachine setup and find what is going wrong.
     
  9. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Sure ! Sounds like a plan, I've sent the project files link to a conversation with you. Thanks for taking peek.
     
  10. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    So after looking at your project, effectively you cannot do a crossfade to 'idle' in this case because you are using synchronized layer which will force a transition to idle everytime the clip normalized time is greater than 1.

    Remove the StateMachineBehaviour that I told you to add. It does nothing good in this case.
    What I would do in this case is change the layer weight when the Fire button state change.

    Code (CSharp):
    1.    
    2. void Update () {
    3.         bool isShooting = Input.GetButton("Fire1");
    4. //Crouch Attack
    5.         if ((fireRate == 0) && Input.GetAxisRaw ("Vertical") < 0 && ground) {
    6.             if (Input.GetButton("Fire1"))
    7.             {
    8.                 Shoot ();
    9.                 anim.SetTrigger ("cAttack");
    10.             }
    11.         }
    12.         else {
    13.             if (Input.GetButton("Fire1") && Time.time > timeToFire && Input.GetAxisRaw("Vertical") < 0 && ground)
    14.             {
    15.                 timeToFire = Time.time + 1 / fireRate;
    16.                 Shoot ();
    17.                 anim.SetTrigger ("cAttack");
    18.             }
    19.         }
    20. //END Crouch Attack
    21.        
    22. //Basic Attack
    23.         if (fireRate == 0) {
    24.             if (Input.GetButton("Fire1"))
    25.             {
    26.                 Shoot ();
    27.                 anim.SetTrigger ("Attack");
    28.             }
    29.         }
    30.         else {
    31.             if (Input.GetButton("Fire1") && Time.time > timeToFire)
    32.             {
    33.                 timeToFire = Time.time + 1 / fireRate;
    34.                 Shoot ();
    35.                 anim.SetTrigger ("Attack");
    36.             }
    37.         }
    38. //END Basic Attack
    39.         ground = (GameObject.FindGameObjectWithTag ("Player").GetComponent<Player2> ().ground);
    40.  
    41.         anim.SetLayerWeight(1, isShooting ? 1 : 0);
    42.     }
    43.     void Shoot () {
    44.         GameObject obj = ObjectPooler.current.GetPooledObject ();
    45.         if (obj == null) return;
    46.         obj.transform.position = firePoint.transform.position;
    47.         obj.transform.rotation = firePoint.transform.rotation;
    48.         obj.SetActive(true);
    49.     }
    I did change your Input.GetButtonDown to Input.GetButton to get auto fire like behaviour.
     
  11. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Wow , feels like we are getting so close! So here's my next hiccup:

    Holding the attack button is supposed to charge an attack, so having the auto fire function wont be an option for me. It does look fantastic if it were an option though haha.

    Now it still looks good as far as If I do use "GetButtonDown" at lease he holds his gun arm out to start charging, but If i rapid click to fire (as auto isn't an option) then he gets stuck in the awkward switching back and forth from gun and hand between shots.
    So I guess I am wondering , got any tricks to delay the LayerWeight resetting since i can't click faster then it updates frames?
    That seems to be what i was trying at the beginning of this thread with a co-routine haha.:D
     
  12. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
  13. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Gosh this is killing me, I've tried the StopCoroutine all over my attack script. I can either get it to stop the coroutine 100% of the time which traps my character in the attack state as the coroutine never happens. Or I can get it to stop the coroutine 0% of the time, and which case I get the queuing of coroutines creating the annoying flickering between states that doesn't work either.

    Code (CSharp):
    1.  
    2. Animator anim;
    3. public IEnumerator endShoot;
    4.  
    5. void Update(){
    6. endShoot = EndShoot();
    7.  
    8. if (Input.GetButtonDown ("Fire1")) {
    9.                 Shoot ();
    10.             }
    11. }
    12. void Shoot () {
    13. StopCoroutine (endShoot);
    14.         GameObject obj = ObjectPooler.current.GetPooledObject ();
    15.         if (obj == null) return;
    16.         obj.transform.position = firePoint.transform.position;
    17.         obj.transform.rotation = firePoint.transform.rotation;
    18.         obj.SetActive(true);
    19.  
    20.         anim.SetLayerWeight (1,1);
    21.         StartCoroutine (endShoot);
    22.     }
    23.     IEnumerator  EndShoot(){
    24.         yield return new WaitForSeconds (0.4f);
    25.         anim.SetLayerWeight (1,0);
    26.     }
    27.  
    I thought if I put it in this order I could have the Shoot () function turn off any previous instance of the coroutine and then turn a new instance on so that we'd get the off called just once when finished mashing the fire key.. But that isn't working. I tried attaching the StopCoroutine to the Input as well, but also unsatisfactory result.
     
  14. Mecanim-Dev

    Mecanim-Dev

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    1,675
    If can't get the result you want with coroutine you could try to emulate the same behaviour in Update(), simply store the time in a variable when you start to shot and when enough time is elapsed simply set your layer weight to 0
     
  15. ScratchModed

    ScratchModed

    Joined:
    Jun 5, 2015
    Posts:
    24
    Hey! Alright ! That was the hint I needed man, Thanks!

    Code for others who might be curious:
    Code (CSharp):
    1. float sTime = 1;
    2. float cooldown = 0.5f;
    3.  
    4. void Update () {
    5.         bool isCharging = Input.GetButton("Fire1");
    6.         if (isCharging) {
    7.             anim.SetLayerWeight(1,1);
    8.         }
    9.         if (Time.time >= sTime + cooldown && !isCharging) {
    10.             anim.SetLayerWeight (1,0);
    11.         }
    12. }
    13.  
    14. void Shoot () {
    15.         GameObject obj = ObjectPooler.current.GetPooledObject ();
    16.         if (obj == null) return;
    17.         obj.transform.position = firePoint.transform.position;
    18.         obj.transform.rotation = firePoint.transform.rotation;
    19.         obj.SetActive(true);
    20. //Setting Attack Sync layer to viewable and then turning it off (Layer#, Weight)
    21.         anim.SetLayerWeight (1,1);
    22.         sTime = Time.time;
    23. }
    Shoot puts me in attack layer, charging lets me hold the button and stay on attack layer, and sTime + Cooldown take me out of the attack layer if I haven't attacked again within the cooldown time.
     
    socialzombie and Mecanim-Dev like this.