State-Machine-based combo system isn't working in a seemingly-impossible manner

Ho’ there!

I’m working on some first-person combat using a State Machine system that operates via a CurrentState Enum and If statements referencing said Enum. It works like this:

function Update () {
    if(currentState == CharacterStates.whateverState){
              Do State Stuff
    }

}

And state changes are handled within each state, giving me control over what you can do and when you can do it:

  if(currentState == CharacterStates.whateverState){
    if(Input.GetButtonDown("whateverButton"){
       currentState = CharacterStates.correspondingState;
    }
  }

It seemed like a pretty solid system and worked pretty well, until I put in a combo system. The system isn’t operating properly during the combo, which seems impossible based on the programming I’ve done.

This is the code I have for a Three-Strike Combo, without extraneous bits for dodging and such:

function Update () {

    if(!canQueue){
        attackSlashQueued = false;
    }

//IDLE STATE________________________________________________________________________________
    if(currentState == CharacterStates.idle){
        if(Input.GetButtonDown("Attack")){
            currentState = CharacterStates.slash1;
        }
    }


//SLASH 1 STATE________________________________________________________________________________
    if(currentState == CharacterStates.slash1){

        animationComponent.Play("Attack_Slash1");

    //CANCELS/QUEUES___________________________________________________________________

        if(canQueue){
            if(Input.GetButtonDown("Attack"))
                attackSlashQueued = true;
        }

        if(canCancel){
            if(attackSlashQueued){
                currentState = CharacterStates.slash2;
            }
        }


        if(currentAnimationFrame > 2 && animationIsDone){
            currentState = CharacterStates.idle;
        }

    }

//SLASH 2 STATE________________________________________________________________________________
    if(currentState == CharacterStates.slash2){

        animationComponent.Play("Attack_Slash2");

    //CANCELS/QUEUES___________________________________________________________________

        if(canQueue){
            if(Input.GetButtonDown("Attack"))
                attackSlashQueued = true;
        }

        if(canCancel){
            if(attackSlashQueued){
                currentState = CharacterStates.slash3;
            }
        }


        if(currentAnimationFrame > 2 && animationIsDone){
            currentState = CharacterStates.idle;
        }

    }

//SLASH 3 STATE________________________________________________________________________________
    if(currentState == CharacterStates.slash3){

        animationComponent.Play("Attack_Slash3");

    //CANCELS/QUEUES___________________________________________________________________

        if(canQueue){
            if(Input.GetButtonDown("Attack"))
                attackSlashQueued = true;
        }

        if(canCancel){
            if(attackSlashQueued){
                currentState = CharacterStates.slash2;
            }
        }


        if(currentAnimationFrame > 2 && animationIsDone){
            currentState = CharacterStates.idle;
        }

    }
}

Here’s the breakdown:

-The general state is Idle, where all movement is handled for now.

-If you click the Attack Button in the Idle State, it changes the State to the Slash1 State

-The SlashX States handle the entirety of the attack specifics, like the animation, sounds, and such

-I have toggles baked into the animations that tell the script when you can Queue up an action, and when to execute on that Queued action.

-In theory, if you click the Attack Button in the Slash1 State, it should change the State to the Slash2 State, and it does.

-In the Slash2 State, pressing the Attack Button should change the State to Slash3, and pressing Attack in Slash3 goes back to Slash2, making it an infinite combo.

Here’s the strange part, though: with the current setup, pressing the Attack Button in Slash2 doesn’t make it Slash3, but it replays the Slash2 animation and stays in the Slash2 State until I let it complete, after which is goes back to the Idle State.

I’ve been doing testing with changing the order the States combo in, as well as changing which animation plays in which State, and it looks like the script just refuses to go into the Slash3 State.

It’s super weird- anyone see anything I’m missing?

You have to define all the combos into a list of combos.
Something like this…

public enum eAttack
    {
        Slash1 = 0,
        Slash2,
        Slash3,
        Slash4//etc...
    }

    public class cCombos
    {
        public int Index { get; set; }
        public List<eAttack> Attacks { get; set; }
        public float ExtraDamage { get; set; }

        public cCombos()
        {
            Attacks = new List<eAttack>();
            ExtraDamage = 0;
        }

        public cCombos(int index, List<eAttack> attacks, float extraDamage)
        {
            Index = index;
            Attacks = attacks;
            ExtraDamage = extraDamage;
        }

    }

After that you should check if the combo is made, something like this (this program i didnt test it)

List<cCombos> combos = new List<cCombos>();
        cCombos currentAttackCombo = new cCombos();
        float timeoutCombo = 0f;
        float clearComboTimer = 0.5f;

        void Start()
        {
            combos.Add(new cCombos(0, new List<eAttack>() { eAttack.Slash1, eAttack.Slash3 }, 2));
            combos.Add(new cCombos(1, new List<eAttack>() { eAttack.Slash1, eAttack.Slash4, eAttack.Slash2 }, 30));
            combos.Add(new cCombos(2, new List<eAttack>() { eAttack.Slash1, eAttack.Slash1, eAttack.Slash2 }, 100));
        }

        void Update()
        {
            timeoutCombo += Time.deltatime;

            if (key attack pressed)
            {
                timeoutCombo = 0f;
            }

            if (timeoutCombo > clearComboTimer)
            {
                timeoutCombo = 0f;
                currentAttackCombo.Attacks.Clear();
            }

            currentAttackCombo.Attacks.Add(currentState);
            int comboIndex = ComboComplete();
            if (comboIndex != -1)
            {
                //Apply Combo
                int extraDmg = combos.Where(x => x.Index == comboIndex).FirstOrDefault().Index;
                currentAttackCombo.Attacks.Clear();
            }
            else
            {
                //Apply normal damage
            }
        }

        int ComboComplete()
        {
            //some lambda expresion
            var cmbList = combos.Where(x => x.Attacks.Count == currentAttackCombo.Attacks.Count);//only the one with same length
            foreach (var cmb in cmbList)
            {
                for (int i = 0; i < cmb.Attacks.Count; i++)
                {
                    bool foundCombo = true;
                    if (cmb.Attacks[i] != currentAttackCombo.Attacks[i])
                    {
                        foundCombo = false;
                        break;
                    }
                    if (foundCombo)
                    {
                        return cmb.Index;
                    }
                }
            }
          
            return -1;
        }

I added a timeout combo.

You can delete this timer and modify the function ComboComplete to return -1 combo failed and -2 for combo not completed and waiting :slight_smile: but you need to modify the logic of this method.

1 Like

Thanks for the response!

Looks like I forgot to add the Enum into my code example, but I do have all of the relevant states defined:

enum CharacterStates{idle, block, blockIn, blockOut, dodgeBack, dodgeForward, dodgeLeft, dodgeRight, duck, jump, slash1, slash2, slash3};

var currentState : CharacterStates;

I appreciate the effort you put into writing that system, but I’m keen on sticking with the one I’ve got right now (I already have a bunch of states written out). Just need to figure out why the Slash2>Slash3 transition isn’t working.

Line81

currentState = CharacterStates.slash2;

Indeed- it should go from 1>2>3>2>3>etc… and repeat the latter two attacks, as the first one is more of a startup attack.

Thing is it should be actually performing the stuff in the Slash3 State before going back to Slash2- it can’t transition unless canQueue and canCancel are true, but they’re made false at the beginning of each animation.

I’ve literally copy-pasted the contents of the working States into the Slash3 State, so it’s not the coding.

But I’ve also tried changing the animation, and it still happens there, so it’s not the animation…

It’s just strange because there’s literally nothing in the Slash2 State that allows it to go back into Slash2, and even if it did, it wouldn’t repeat it like it’s doing because it never leaves Slash2