Combo Attacks with switching by pressing

Hello all

as i am going over my bugs wanted maybe to know how would you Handle the combos with different attacks
by clicking the mouse button to each combo to change

here is how i did it
it sometimes play the first attack twice so im trying to see how i can use the normalizeTime to use it better

public class PlayerAttackState : PlayerBaseState
    {
        bool isLightAttackPressed = false;
        bool isHeavyAttackPressed = false;
        int attackIndex = 0;

        public PlayerAttackState(PlayerStateMachine stateMachine, int attackIndex) : base(stateMachine)
        {
            this.attackIndex = attackIndex;
        }

        public override void Enter()
        {
            stateMachine.inputHandler.lightAttackEvent += OnLightAttack;
            stateMachine.inputHandler.heavyAttackEvent += OnHeavyAttack;

            stateMachine.fighter.HandleAttack(attackIndex);
            //movement.KnockBack();
        }

        public override void Exit()
        {
            stateMachine.inputHandler.lightAttackEvent -= OnLightAttack;
            stateMachine.inputHandler.heavyAttackEvent -= OnHeavyAttack;
        }

        public override void Executue(float deltaTime)
        {
            if (stateMachine.animatorHandler.isInteracting) return;
            if (!stateMachine.fighter.isInCombo && (isHeavyAttackPressed || isLightAttackPressed))
            {
                HandleCombo();
            }
            else
            {
                ReturnToLocomotion();
            }
        }

        private void HandleCombo()
        {
            HandleLightAttack();
            HandleHeavyAttack();
        }

        private void HandleLightAttack()
        {
            if (isLightAttackPressed)
            {
                //isLightAttackPressed = false;
                if (attackIndex > 1)
                {
                    
                    stateMachine.SwitchState(new PlayerAttackState(stateMachine, 0));
                    return;
                }
                stateMachine.SwitchState(new PlayerAttackState(stateMachine, attackIndex + 1));
                return;
            }
        }

        private void HandleHeavyAttack()
        {
            if (isHeavyAttackPressed)
            {
                //isHeavyAttackPressed = false;
                if (attackIndex < 3 || attackIndex > 4)
                {
                    stateMachine.SwitchState(new PlayerAttackState(stateMachine, 3));
                    return;
                }

                stateMachine.SwitchState(new PlayerAttackState(stateMachine, attackIndex + 1));
                return;
            }
        }

        private void OnHeavyAttack()
        {
            if (stateMachine.fighter.isInCombo)
            {
                isHeavyAttackPressed = true;
            }
        }

        private void OnLightAttack()
        {
            if (stateMachine.fighter.isInCombo)
            {
                isLightAttackPressed = true;
            }
        }
    }

isInCombo gets updated by an animation event so enable combo in the certain amount of time in the animation to get a click for light attack or heavy attack

then to see if light attack or heavy attack is pressed
and then switch to which attack

Fighter holds all the attacks light attacks and heavy attacks

if you want more info or other suggestions how to handle this let me know

It sounds like the issue is that sometimes the 1st attack is repeated instead of moving on to the combo attack. The setup is different enough that I’m going to have to make some assumptions about part of what’s going on.

Firstly, you have two different states which can be in direct conflict with one another. For instance, what happens if both the Light Attack button and the Heavy Attack button are pressed at the same time? My analysis of the code suggests that the stateMachine will attempt to switch to a new AttackState with a light attack value, then immediately switch to a new state with a heavy attack value. This can be rectified by resetting the opposing boolean in the OnHeavyAttack() and OnLightAttack() methods, for example:

private void OnHeavyAttack()
{
    if(stateMachine.fighter.isInCombo)
    {
          isHeavyAttackPressed=true;
          isLightAttackPressed=false;
     }
}

and doing the same thing in isLightAttack.
It’s also advisable to short circuit HandleCombo if LightAttack is selected, then we don’t want to try to switch states in HandleHeavyAttack() at all. Changing HandleLightAttack() to a bool method will allow this:

private void HandleCombo()
        {
            if(HandleLightAttack()) return;
            HandleHeavyAttack();
        }

        private bool HandleLightAttack()
        {
            if (isLightAttackPressed)
            {
                //isLightAttackPressed = false;
                if (attackIndex > 1)
                {
                    
                    stateMachine.SwitchState(new PlayerAttackState(stateMachine, 0));
                    return true;
                }
                stateMachine.SwitchState(new PlayerAttackState(stateMachine, attackIndex + 1));
                return true;
            }
            return false; //LightAttack was not true
        }

Finally, you appear to have two competing mechanisms for determining whether to check for in combo… I don’t know what the conditions are for animatorHandler.isInteracting… is this a certain percentage of GetNormalizedTime?
In any event, you’ve already factored in the isInCombo, as isHeavyAttackPressed and isLightAttackedPressed cannot be set to true unless fighter.isInCombo. Testing for it again risks that isInCombo may now be false but the button was tested at the correct time.

We need to know when the animation is finished or nearly finished (or we’ll fall out of AttackState prematurely), so since we’ve already determined that the player pressed the button at the correct time, and we know that by the end of the animation we should no longer be testing for a combo, I would simply simplify that 2nd check in Executue (tick) to:

if(isHeavyAttackPressed || isLightAttackpressed)

the is interacting i made to reset a bool in the animator when finished play the animation
because the GetNormalizedTime here isnt working for me

Because have different attacks and you want to switch when the animation is finished after logging that you pressed a button

i got all messed up with the light attack press and enabling combo withing the animation event lol

so i have enable colliders disable colliders and enable combo in the middle of animation and disable combo in the end of the animation

i dont understand how i can use here the GetNormalizedTime to help me with it
im not using it here at all

The problem im getting is that it returns to Locomotion too fast and then enter again
so my way of knowing if it finished is wrong

how can i use the GetNormalizedTime here i want to take out the isInteracting

I have the same issue with doing a roll dodge as using root motion
i also have to change the rotation of the character
so i am using the isInteracting to know that now im doing an action

its working good but with the precisions of doing attacks and combos sometimes you get in the first light or heavy attack 2 attacks very fast like a start of an attack then do another attack

Brian i found my problem with the isInteracting i will show you

so for me to know when i do some interacting ( some kind of action i have this )

I put a comment where i add that line now and all is good

public void PlayTargetAnimation(string targetAnim, bool isInteracting, bool useRootMotion = false)
        {
            if (useRootMotion)
            {
                animator.applyRootMotion = true;
            }
            else
            {
                animator.applyRootMotion = false;
            }
            this.isInteracting = isInteracting; // Here i forgot to add 
            animator.SetBool(interactingHash, isInteracting);
            animator.CrossFadeInFixedTime(targetAnim, animatorDampTime);
        }

so i have this method in animator handler and i made another layer for all my actions
like open a door open a chest attacking jumping and what ever
i reset the bool with OnStateExit of the animator

And in LateUpdate i get the bool to update isInteracting

i hope you understand what i mean lol :]

And again if you have a better more cleaner way then this im all ears

To be honest, that confused me more than it cleared things up.

It might be easiest to recap how GetNormalizedTime works, as it doesn’t rely on triggers and a lot of state you’re passing around.

Each Animation state should have a tag with “Attack” on it.


This is lets the GetNormalizedTimeRemaining tell the percentage of time left in the animation whether it’s Attack1, Attack2, or whatever. As long as the tag is Attack, the function will find it.

Here’s my slightly modified version of GetNormalizedTime() which is also handy for other types of actions:

        protected float GetNormalizedTime(Animator animator, string tagToCheck = "Attack")
        {
            AnimatorStateInfo currentInfo =animator.GetCurrentAnimatorStateInfo(0);
            AnimatorStateInfo nextInfo =animator.GetNextAnimatorStateInfo(0);
            if (animator.IsInTransition(0) && nextInfo.IsTag(tagToCheck))
            {
                return nextInfo.normalizedTime;
            } else if (!animator.IsInTransition(0) && currentInfo.IsTag(tagToCheck)) 
            {
                return currentInfo.normalizedTime;
            }
            return 0;
        }

My modification makes it so that you can put the method in the base class and pass the required tag to the method. I find it handy for things like Dodge and roll moves as well.

With the tag on the method, you can tell exactly what percentage of time is remaining in the animation. That’s what Normalized time is… it gives a number between 0 (just starting) and 1 (done). Now we don’t need to worry about setting bools and state, just check to see if (GetNormalizedTime(animator, "Attack") > .9f)

Sorry got you confused
I made another layer with all my actions which have different states like light attack 1 light attack 2 jump dodge etc
And there have an empty state that i send all my transitions to there
When it enter the state of empty after it does its action/attack/etc

It goes to the empty state and reset the bool isInteracting
So when you are doing an action you know you doing it

But anyway i will try do it with the normalizetime seems to have more control of the time of the animations

This may be moving far enough away from the course that I’m not sure I’m going to be able to help…

Its ok its working but i will try do with the normalizetime :wink:

Just remember, if you’re running these animations on another layer, you need to pass that layerIndex in the the GetNormalizedTime() method… it’s likely that it wasn’t working for you because the states aren’t in the 0th (default) layer, but in the 1 layer, i.e.

AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(1);

Yeah probably because i started making all this state machine before i had the course
As usual learning a lot from here :wink:

So brian after working on it a little at my spare time after work i have come up with a better solution using GetNormalizedTime()

And moving the attacking animation and other actions animations to the Base Layer

So on my AnimatorHandler has this

public bool IsInAction()
        {
            return GetNormalizedTime(animator, actionTag) < 0.9f;
        }


        public float GetNormalizedTime(Animator animator, string tagToCheck)
        {
            AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
            AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);
            if (animator.IsInTransition(0) && nextInfo.IsTag(tagToCheck))
            {
                return nextInfo.normalizedTime;
            }
            else if (!animator.IsInTransition(0) && currentInfo.IsTag(tagToCheck))
            {
                return currentInfo.normalizedTime;
            }
            else
            {
                return 0f;
            }
        }

Now if i want more control i could even add a float of actionTime and send depending on the animation action time

and then in my PlayerAttackState which i can also do in my EnemyAttackState as animatorHandler is a class that has to each character/npc/player/enemy

public class PlayerAttackState : PlayerBaseState
    {
        bool isLightAttackPressed = false;
        bool isHeavyAttackPressed = false;
        int attackIndex = 0;

        public PlayerAttackState(PlayerStateMachine stateMachine, int attackIndex) : base(stateMachine)
        {
            this.attackIndex = attackIndex;
        }

        public override void Enter()
        { 
            stateMachine.inputHandler.lightAttackEvent += OnLightAttack;
            stateMachine.inputHandler.heavyAttackEvent += OnHeavyAttack;
            stateMachine.fighter.HandleAttack(attackIndex);
        }

        public override void Exit()
        {
            stateMachine.inputHandler.lightAttackEvent -= OnLightAttack;
            stateMachine.inputHandler.heavyAttackEvent -= OnHeavyAttack;
        }

        public override void Executue(float deltaTime)
        {
            stateMachine.mover.MoveTo(Vector3.zero, deltaTime);
            if (!stateMachine.animatorHandler.IsInAction())
            {
                if (isHeavyAttackPressed || isLightAttackPressed)
                {
                    HandleCombo();
                }
                else
                {
                    ReturnToLocomotion();
                }
            }
        }

        private void HandleCombo()
        {
            if (HandleLightAttack()) return;
            HandleHeavyAttack();
        }

        private bool HandleLightAttack()
        {
            if (isLightAttackPressed)
            {
                if (attackIndex > 1)
                {

                    stateMachine.SwitchState(new PlayerAttackState(stateMachine, 0));
                    return true;
                }
                stateMachine.SwitchState(new PlayerAttackState(stateMachine, attackIndex + 1));
                return true;
            }
            return false;
        }

        private void HandleHeavyAttack()
        {
            if (isHeavyAttackPressed)
            {
                if (attackIndex < 3 || attackIndex > 4)
                {
                    stateMachine.SwitchState(new PlayerAttackState(stateMachine, 3));
                    return;
                }

                stateMachine.SwitchState(new PlayerAttackState(stateMachine, attackIndex + 1));
                return;
            }
        }

        private void OnHeavyAttack()
        {
            if (stateMachine.animatorHandler.IsInAction())
            {
                isHeavyAttackPressed = true;
                isLightAttackPressed = false;
            }
        }

        private void OnLightAttack()
        {
            if (stateMachine.animatorHandler.IsInAction())
            {
                isLightAttackPressed = true;
                isHeavyAttackPressed = false;
            }
        }
    }

Anyway its working great and now i can move on to more stuff :]

Edited i add the actionTime float and now i control the dodge roll more smoothly :]

1 Like

Hi, I really want to learn your code. What does the stateMachine.fighter.HandleAttack do?

i still need to do some refactor here but this is what it does

public void HandleAttack(int attackIndex)
        {
            currentTarget = null;
            currentAttackIndex = attackIndex;
            animatorHandler.PlayTargetAnimation(attacks[attackIndex].attackAnimation, true);
            forceReciver.AddForce(transform.forward * attacks[attackIndex].attackForce);
        }

setting my currentTarget to null my target in fighter gets updated when he hit a target
setting the currentAttack index so i can get from my List of AttackData the info
And plays the animation from the animatorHandler which now still has true for isInteracting which i change
so i need to do a refactor for that method

then adds force like we did in the course

hope that helps

It helps me a lot. Thanks!

My code is a bit different then the course i have different components and how i use stuff