Attack Combo issues

Hi,

I decided going into the course that I wanted to have the player tap a button rather than hold to execute the attack action as well as give the player two ways of doing damage, one being light damage, with the other being heavy damage.

At first I put both the light and heavy animations in one array (exactly as it was done in the course) and had the player attacking switch state function in both the targeting and free look script dictate which damage type was being used by keeping the 0 as it was for the Light Attack button and changing the 0 to a 3 if the Heavy Attack Button was selected. However while everything else in the melee combat section of this course worked, I was unable to get the player to perform anything outside of the first animation for both Light and Heavy.

Presently, I decided to create two arrays in the player state machine, one that holds the light attack damage and another for the heavy attack damage as I have a few individuals do the same thing. But, more errors have occurred with this method than the previous one to the point where I question if I should revert back to the previous iteration. Upon attacking using both Light and Heavy Attack Button, only the Heavy Attack animation plays, and once again, it is only the first heavy animation. I tried coming up with a method as I have seen those before do but to no avail do they work and now they’re just blank.

I suspect the problem comes from the the cluster of items in enter function as well as an oversight in the tick function

(Apologies, this is my first, I didn’t know how to format the code for a post)

public class PlayerAttackingState : PlayerBaseState
{
    private float previousFrameTime;
    private bool alreadAppliedForce;
    private Attack attack;
    private Attack heavyAttack;
 
    public PlayerAttackingState(PlayerStateMachine stateMachine, int attackIndex) : base(stateMachine)
     {
         attack = stateMachine.Attacks[attackIndex]; 
         heavyAttack = stateMachine.HeavyAttacks[attackIndex];
     }
 
     public override void Enter() 
     {
         stateMachine.InputReader.LightAttackDown += OnLightAttackDown;
         stateMachine.InputReader.HeavyAttackDown += OnHeavyAttackDown;
         stateMachine.Weapon.SetAttack(attack.Damage); 
         stateMachine.Weapon.SetAttack(heavyAttack.Damage); 
         stateMachine.Animator.CrossFadeInFixedTime(attack.AnimationName, attack.TransitionDuration); 
         stateMachine.Animator.CrossFadeInFixedTime(heavyAttack.AnimationName, heavyAttack.TransitionDuration);
     }
 
     public override void Tick(float deltaTime)
      {
         Move(deltaTime);
         FaceTarget(); 
         float normalizedTime = GetNormalizedTime(); 
         if(normalizedTime < 1f)
         {
             if(normalizedTime >= attack.ForceTime)
             {
                 TryApplyForce();
             }
        } 
         else
        { 
             if(stateMachine.Targeter.CurrentTarget != null) 
             { 
                 stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
             }
             else
             {
                 stateMachine.SwitchState(new PlayerFreeLookState(stateMachine)); 
             } 
         }

         previousFrameTime = normalizedTime; 
     }
 
     public override void Exit()
     {
         stateMachine.InputReader.LightAttackDown -= OnLightAttackDown; 
         stateMachine.InputReader.HeavyAttackDown -= OnHeavyAttackDown; 
     } 

     private float GetNormalizedTime() 
     { 
         AnimatorStateInfo currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0); 
         AnimatorStateInfo nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);
         if(stateMachine.Animator.IsInTransition(0) && nextInfo.IsTag("Attack"))
         {
             return nextInfo.normalizedTime; 
         }
         else if(!stateMachine.Animator.IsInTransition(0) && currentInfo.IsTag("Attack")) 
         { 
             return currentInfo.normalizedTime;
         } 
         else
         { 
            return 0f;
         }
      }
 
     private void TryComboAttack(float normalizedTime) 
     {
         if(attack.ComboStateIndex == -1) {return;}
         if (normalizedTime < attack.ComboAttackTime) {return;}
         stateMachine.SwitchState 
         (
             new PlayerAttackingState 
             (
                 stateMachine,
                 attack.ComboStateIndex
            )
         );
     }
 
     private void TryApplyForce()
     {
         if(alreadAppliedForce) { return; }
         stateMachine.ForceReceiver.AddForce(stateMachine.transform.forward * attack.Force);
         alreadAppliedForce = true;
     }
 
     private void OnLightAttackDown()
     {
     }
 
     private void OnHeavyAttackDown()
     {
     } 
 }
public override void Enter()

{

    stateMachine.InputReader.LightAttackDown += OnLightAttackDown;

    stateMachine.InputReader.HeavyAttackDown += OnHeavyAttackDown;

    stateMachine.Weapon.SetAttack(attack.Damage);

    stateMachine.Weapon.SetAttack(heavyAttack.Damage);

    //this
    stateMachine.Animator.CrossFadeInFixedTime(attack.AnimationName, attack.TransitionDuration);

    //this
    stateMachine.Animator.CrossFadeInFixedTime(heavyAttack.AnimationName, heavyAttack.TransitionDuration);

}

This is what is causing only the heavy attack to play — when you Enter this state, you tell unity “Hey, cross frade into the light attack animation” (stateMachine.Animator.CrossFadeInFixedTime(attack.AnimationName, attack.TransitionDuration)

… no wait actually, cross fade into the heavy attack animation (stateMachine.Animator.CrossFadeInFixedTime(heavyAttack.AnimationName, heavyAttack.TransitionDuration)

So no matter what you do, that second crossfade will always play.

I am sure there are 100 ways to do this, but it might be worth adding into your constructor some indicator if you are entering the state with a heavy or a light attack, and then doing an if() to cross-fade into the correct animation.

You can use that same if statement to subscribe to the light/heavy on press (maybe for your first iteration to get it working, only allow them to combo light attack → light attack , aka if they start with a light attack, they can only combo light attacks … if they start with heavy, they can only combo heavy)

int attackType;

// attackType : int where 0 = light attack and 1 = heavy attack
public PlayerAttackingState(PlayerStateMachine stateMachine, int attackIndex, int attackType) : base(stateMachine)
{
this.attackType = attacktype;
if(attackType == 0) //go into light attack animation, subscribe to light attack button pressed, etc.
else if(attackType == 1) // go into heavy attack animation, subscribe to heavy attack button pressed, etc.
}

Then, in your OnLightAttackDown , you can just call TryComboAttack
Within TryComboAttack you can do:

if (normalizedTime < attack.ComboAttackTime) {return;}

stateMachine.SwitchState (  new PlayerAttackingState ( stateMachine,attack.ComboStateIndex, attackType) );

(NOTE: Without compiling it myself this may not be 100% accurate, please forgive, I’m learning too!)

As far as it only playing the FIRST animation, i would check your inspector and make sure that the ComboStateIndex is correct:

If you array of attacks looks like this:
Index 0:
Anim Name: Light Attack
ComboStateIndex: 0 ← when we combo, go to this state
Index 1:
Anim Name: Light Attack Combo
ComboStateIndex: 2 ← when we combo, go to this state

Then it will constantly play the first animation over and over again.
Index 0:
Anim Name: Light Attack
ComboStateIndex: 1 ← when we combo, go to this state
Index 1:
Anim Name: Light Attack Combo
ComboStateIndex: 2 ← when we combo, go to this state

public class PlayerAttackingState : PlayerBaseState

{

private float previousFrameTime;

private bool alreadAppliedForce;

private Attack attack;

private Attack heavyAttack;

float normalizedTime;

int attackType;

public PlayerAttackingState(PlayerStateMachine stateMachine, int attackIndex, int attackType) : base(stateMachine)

{

    this.attackType = attackType;

    if(attackType == 0)

    {

        stateMachine.InputReader.LightAttackDown += OnLightAttackDown;

        stateMachine.Animator.CrossFadeInFixedTime(attack.AnimationName, attack.TransitionDuration);

    }

    else if (attackType == 1)

    {

        stateMachine.InputReader.HeavyAttackDown += OnHeavyAttackDown;

        stateMachine.Animator.CrossFadeInFixedTime(heavyAttack.AnimationName, heavyAttack.TransitionDuration);

    }

}

public override void Enter()

{

    stateMachine.Weapon.SetAttack(attack.Damage);

    stateMachine.Weapon.SetAttack(heavyAttack.Damage);

}

public override void Tick(float deltaTime)

{

    Move(deltaTime);

    FaceTarget();

    float normalizedTime = GetNormalizedTime();

    if(normalizedTime < 1f)

    {

        if(normalizedTime >= attack.ForceTime)

        {

            TryApplyForce();

        }

    }

    else

    {

        if(stateMachine.Targeter.CurrentTarget != null)

        {

            stateMachine.SwitchState(new PlayerTargetingState(stateMachine));

        }

        else

        {

            stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));

        }

    }



    previousFrameTime = normalizedTime;

}

public override void Exit()

{

   

}

private float GetNormalizedTime()

{

    AnimatorStateInfo currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0);

    AnimatorStateInfo nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);

    if(stateMachine.Animator.IsInTransition(0) && nextInfo.IsTag("Attack"))

    {

        return nextInfo.normalizedTime;

    }

    else if(!stateMachine.Animator.IsInTransition(0) && currentInfo.IsTag("Attack"))

    {

        return currentInfo.normalizedTime;

    }

    else

    {

        return 0f;

    }



}

private void TryComboAttack()

{

    if(attack.ComboStateIndex == -1) {return;}

    if (normalizedTime < attack.ComboAttackTime) {return;}

    stateMachine.SwitchState (new PlayerAttackingState (stateMachine, attack.ComboStateIndex, attackType));

}

private void TryApplyForce()

{

    if(alreadAppliedForce) { return; }

    stateMachine.ForceReceiver.AddForce(stateMachine.transform.forward * attack.Force);

    alreadAppliedForce = true;

}

private void OnLightAttackDown()

{

    TryComboAttack();

}

private void OnHeavyAttackDown()

{

    TryComboAttack();

}

}

I haven’t run it yet due to another problem I can’t wrap my head around.

private void OnLightAttackDown()

{

    stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));

}

private void OnHeavyAttackDown()

{

    stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));

}

This code can be found in both my Freelook and Targeting scripts and as crude as the idea was, they are what allow me to switch to the Attacking State. As soon as I had added the attackType into the constructor of the PlayerAttackingState as you advised, they keeping flagging the error

There is no argument given that corresponds to the required parameter ‘attackType’ of ‘PlayerAttackingState.PlayerAttackingState(PlayerStateMachine, int, int)’

I realise to get rid of this error it is easier to get rid of these lines all together and add an ‘if’ statement in the Tick function as we have done previously to switch the states with the Freelook and the Targeting states. My issue is that I have attempted to do so, which is what led me to such a crude method in the first place.

Additonally I might have misinterpreted a few things. As demonstrated I tweaked the constructor as well as adding in the Int attackType, however, when you said subscribe to the respective attacks, I put both the OnLightAttackDown and OnHeavyAttackDown as well as the CrossFadeInFixedTime into their respective ‘If’ statements, was I supposed to that or was that a mistake.

Regarding the inspector, it has been the bane of my existence on this course. Every time I have an error, it is because I forgot to put a game object or a script into its designated location. This is what it looks like

For the most part I think I’m correct, I double checked it against what you said and it looks right. To make sure I am understanding it correctly, this is what the ComboStateIndex is supposed to look like if you have a three part combo; 1 :arrow_forward: 2 :arrow_forward: -1

(Also, no worries, I am also learning, I appreciate any advice and criticism as I am still new to GameDev)

i think in this case, it’s best to create TWO attacking states, one to work on Light attacks, and one to work on Heavy…

In the PlayerFreelook and TargetingState, call the appropriate attack state based on which button was pressed…

void HandleLightAttack()
{
    stateMachine.SwitchState(new PlayerLightAttackState(stateMachine, 0);
}
void HandleHeavyAttack()
{
    stateMachine.SwitchState(new PlayerHeavyAttackState(stateMachine, 0);
}

The two attack states shoud both be fairly similar, exept that they’ll pull from their respective LIghtAttacks or HeavyAttack to get their attack info.

Note that the CrossfadeInFixedTime should be just one in enter, not two, and should simply use the Animation Name from the appropriate attack entry.

In terms of handling switching to a heavy attack from Light attack in vice versa, in LightAttackingState, a Heavy attack should call new PlayerHeavyAttackingState(stateMachine, 0) while a new light attack should call the combo index.
Same way from the heavy attacking state if the light key is pressed, call the lightattack with 0, if the heavy, pass in the Combo state index from the current attack. I’ve included a link demonstrating how to format code in our forums.

Heavy Attack State Example
(The Light Attack State is similar to this)

public class PlayerHeavyAttackingState : PlayerBaseState

{

    private float previousFrameTime;

    private bool alreadAppliedForce;

    private Attack heavyAttack;

    float normalizedTime;

   

    public PlayerHeavyAttackingState(PlayerStateMachine stateMachine, int attackIndex) : base(stateMachine)

    {

        heavyAttack = stateMachine.HeavyAttacks[attackIndex];

    }

    public override void Enter()

    {

        stateMachine.InputReader.HeavyAttackDown += OnHeavyAttackDown;

        stateMachine.Animator.CrossFadeInFixedTime(heavyAttack.AnimationName, heavyAttack.TransitionDuration);

        stateMachine.Weapon.SetAttack(heavyAttack.Damage);

    }

    public override void Tick(float deltaTime)

    {

        Move(deltaTime);

        FaceTarget();

        float normalizedTime = GetNormalizedTime();

        if(normalizedTime < 1f)

        {

            if(normalizedTime >= heavyAttack.ForceTime)

            {

                TryApplyForce();

            }

        }

        else

        {

            if(stateMachine.Targeter.CurrentTarget != null)

            {

                stateMachine.SwitchState(new PlayerTargetingState(stateMachine));

            }

            else

            {

                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));

            }

        }

 

        previousFrameTime = normalizedTime;

    }

    public override void Exit()

    {

        stateMachine.InputReader.HeavyAttackDown -= OnHeavyAttackDown;

    }

    private float GetNormalizedTime()

    {

        AnimatorStateInfo currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0);

        AnimatorStateInfo nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);

        if(stateMachine.Animator.IsInTransition(0) && nextInfo.IsTag("Attack"))

        {

            return nextInfo.normalizedTime;

        }

        else if(!stateMachine.Animator.IsInTransition(0) && currentInfo.IsTag("Attack"))

        {

            return currentInfo.normalizedTime;

        }

        else

        {

            return 0f;

        }

   

    }

    private void TryComboAttack()

    {

        if(heavyAttack.ComboStateIndex == -1) {return;}

        if (normalizedTime < heavyAttack.ComboAttackTime) {return;}

        stateMachine.SwitchState (new PlayerHeavyAttackingState(stateMachine, heavyAttack.ComboStateIndex));

    }

    private void TryApplyForce()

    {

        if(alreadAppliedForce) { return; }

        stateMachine.ForceReceiver.AddForce(stateMachine.transform.forward * heavyAttack.Force);

        alreadAppliedForce = true;

    }

   

    private void OnHeavyAttackDown()

    {

        TryComboAttack();

    }

}

First and foremost thank you for the link for formatting. It was honestly guess work between figuring out if it was ‘Block quotes’ or ‘Preformatted text’. I’ll remember this for future posts!

At first I thought the idea of making it in two different states and scripts was weird when it could all be handled by one script. However, as I was doing it, the more I felt like it was more stupid of me to not put it into separate states and scripts, not only does it make it much cleaner and easier to look at, but a lot of the issues and errors that I had I previously are gone, especially the error that was poking it heads from the Freelook and Targeting states scripts, so thank you very much on that end! :smiley:

My final issue is still unfortunately in regards to why the animation won’t change past the first one for either light or heavy. While the code allows for me to switch seamlessly between light and heavy without any error, it just will not perform the second or third animations that are in queue. I thought at first it was an issue with the inspector as it always is with me, but to my dismay it looks right. I think it might have to do with the fact that I don’t think I’ve set up anything to tell it what to do with the second click onwards, but that’s just a guess. How do you suppose I would go about this?

Are you unsubscribing from HeavyAttackDown and LightAttackDown in both PlayerFreeLookState and PlayerTargetingState?

public override void Exit()

    {
        stateMachine.InputReader.TargetEvent -= OnTarget;
        stateMachine.InputReader.LightAttackDown -= HandleLightAttack;
        stateMachine.InputReader.HeavyAttackDown -= HandleHeavyAttack;
    }

Yes I am. I thought about removing it to see if it would fix the problem, but what would happen instead is that the player upon clicking the either attack button would be able to reset the animation.

No, don’t do that experiment. It’s absolutely vital that all subscribed events in any state be unsubscribed in Exit() or the state will still be hanging out in memory. As long as any class has a refeence to an object, it won’t be picked up by the Garbage Collector.

This is what concerns me. As written, your Heavy attack state will only pick up Heavy Attacks, as it’s subscribed to heavy attacks, but not light attacks… Are there any other classes that are subscribing to these events and switching the state when engaged?

My apologies but I think I might have said the wrong thing to you.

As you said the heavy state is only picking up the heavy attacks as it should, the same can be said for its light counterpart, as the light state is only picking up the light attacks. Both of them at their core are doing what they need to, its just that if you click the light button past the first click it will only do the first light attack animation and will not perform animation two or three, this is the same case for the heavy attack button wherein it will only do the first animation and not the second or third.

Let’s try some informative Debugs:

And in PlayerFreelookState and PlayerTargetingState, add events to the Enter and Exit code just to piont out the order of events…

As you said I implemented the debug.logs in their deisgnated locations. Here are the results:

  • Upon Clicking an attack button, the player the game will log that it is exiting the state, whether that is good or bad it is beyond me.

  • Upon execution of one attack, nothing will happen, however, clicking the the button twice will yield one of the following messages depending on which one you clicked

[19:16:23] Light Attack, CSIndex=0.6, normalizedTime=0
[19:16:24] Heavy Attack, CSIndex=0.5, normalizedTime=0

This message will repeat if the button has been clicked multiple times.

  • Interestingly enough, the second log you asked me to put in does not appear (I believe you might have sensed this to be the case)

As for the order of events chronologically:

[19:16:22] Entering Light/Heavy State

[19:16:22] Exiting Light/Heavy State

[19:16:23] Light Attack, CSIndex=0.6, normalizedTime=0

[19:16:24] Light Attack, CSIndex=0.6, normalizedTime=0

I hope this helps

This actualy helps tremendously

I bet you’re having trouble getting it to fall out of AttackState as well…
Do your attack states in the Animator have the “Attack” on them?

The “Attack” tag? If you mean this then yes, all of them have it.

Ok, this one had me stumped, as all the inputs looked right… so I took one more pass at it, and noticed that in Tick() where you calculate normalizedTime, you declare it as a float. This automatically makes the normalizedTime in Tick() into a local variable which is discarded at the end of Tick() and inaccessible to the TryComboAttack() method.
Remove the float in tick so it’s just

normalizedTime = GetNormalizedTime();

and you should be good to go. (You’ll need to do this in both attacking states)

I can’t tell you how much I am smiling ear to ear right now! While there were other problems with my code it was once again a single word that was out of place that stumped me.

Thank you so much for helping me Brian and apologies for taking up so much of your time. I truly feel like I can carry on with the course without much reservation!

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms