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

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

Discussion in 'Scripting' started by generalmcmutton, Jan 2, 2020.

  1. generalmcmutton

    generalmcmutton

    Joined:
    May 20, 2010
    Posts:
    42
    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:

    Code (JavaScript):
    1. function Update () {
    2.     if(currentState == CharacterStates.whateverState){
    3.               Do State Stuff
    4.     }
    5.  
    6. }
    And state changes are handled within each state, giving me control over what you can do and when you can do it:


    Code (JavaScript):
    1.   if(currentState == CharacterStates.whateverState){
    2.     if(Input.GetButtonDown("whateverButton"){
    3.        currentState = CharacterStates.correspondingState;
    4.     }
    5.   }

    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:

    Code (JavaScript):
    1.  
    2.  
    3. function Update () {
    4.  
    5.     if(!canQueue){
    6.         attackSlashQueued = false;
    7.     }
    8.  
    9. //IDLE STATE________________________________________________________________________________
    10.     if(currentState == CharacterStates.idle){
    11.         if(Input.GetButtonDown("Attack")){
    12.             currentState = CharacterStates.slash1;
    13.         }
    14.     }
    15.  
    16.  
    17. //SLASH 1 STATE________________________________________________________________________________
    18.     if(currentState == CharacterStates.slash1){
    19.  
    20.         animationComponent.Play("Attack_Slash1");
    21.  
    22.     //CANCELS/QUEUES___________________________________________________________________
    23.  
    24.         if(canQueue){
    25.             if(Input.GetButtonDown("Attack"))
    26.                 attackSlashQueued = true;
    27.         }
    28.  
    29.         if(canCancel){
    30.             if(attackSlashQueued){
    31.                 currentState = CharacterStates.slash2;
    32.             }
    33.         }
    34.  
    35.  
    36.         if(currentAnimationFrame > 2 && animationIsDone){
    37.             currentState = CharacterStates.idle;
    38.         }
    39.  
    40.     }
    41.  
    42. //SLASH 2 STATE________________________________________________________________________________
    43.     if(currentState == CharacterStates.slash2){
    44.  
    45.         animationComponent.Play("Attack_Slash2");
    46.  
    47.     //CANCELS/QUEUES___________________________________________________________________
    48.  
    49.         if(canQueue){
    50.             if(Input.GetButtonDown("Attack"))
    51.                 attackSlashQueued = true;
    52.         }
    53.  
    54.         if(canCancel){
    55.             if(attackSlashQueued){
    56.                 currentState = CharacterStates.slash3;
    57.             }
    58.         }
    59.  
    60.  
    61.         if(currentAnimationFrame > 2 && animationIsDone){
    62.             currentState = CharacterStates.idle;
    63.         }
    64.  
    65.     }
    66.  
    67. //SLASH 3 STATE________________________________________________________________________________
    68.     if(currentState == CharacterStates.slash3){
    69.  
    70.         animationComponent.Play("Attack_Slash3");
    71.  
    72.     //CANCELS/QUEUES___________________________________________________________________
    73.  
    74.         if(canQueue){
    75.             if(Input.GetButtonDown("Attack"))
    76.                 attackSlashQueued = true;
    77.         }
    78.  
    79.         if(canCancel){
    80.             if(attackSlashQueued){
    81.                 currentState = CharacterStates.slash2;
    82.             }
    83.         }
    84.  
    85.  
    86.         if(currentAnimationFrame > 2 && animationIsDone){
    87.             currentState = CharacterStates.idle;
    88.         }
    89.  
    90.     }
    91. }


    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?
     
  2. adi7b9

    adi7b9

    Joined:
    Feb 22, 2015
    Posts:
    181
    You have to define all the combos into a list of combos.
    Something like this..
    Code (CSharp):
    1. public enum eAttack
    2.     {
    3.         Slash1 = 0,
    4.         Slash2,
    5.         Slash3,
    6.         Slash4//etc...
    7.     }
    8.  
    9.     public class cCombos
    10.     {
    11.         public int Index { get; set; }
    12.         public List<eAttack> Attacks { get; set; }
    13.         public float ExtraDamage { get; set; }
    14.  
    15.         public cCombos()
    16.         {
    17.             Attacks = new List<eAttack>();
    18.             ExtraDamage = 0;
    19.         }
    20.  
    21.         public cCombos(int index, List<eAttack> attacks, float extraDamage)
    22.         {
    23.             Index = index;
    24.             Attacks = attacks;
    25.             ExtraDamage = extraDamage;
    26.         }
    27.  
    28.     }
    After that you should check if the combo is made, something like this (this program i didnt test it)
    Code (CSharp):
    1. List<cCombos> combos = new List<cCombos>();
    2.         cCombos currentAttackCombo = new cCombos();
    3.         float timeoutCombo = 0f;
    4.         float clearComboTimer = 0.5f;
    5.  
    6.         void Start()
    7.         {
    8.             combos.Add(new cCombos(0, new List<eAttack>() { eAttack.Slash1, eAttack.Slash3 }, 2));
    9.             combos.Add(new cCombos(1, new List<eAttack>() { eAttack.Slash1, eAttack.Slash4, eAttack.Slash2 }, 30));
    10.             combos.Add(new cCombos(2, new List<eAttack>() { eAttack.Slash1, eAttack.Slash1, eAttack.Slash2 }, 100));
    11.         }
    12.  
    13.         void Update()
    14.         {
    15.             timeoutCombo += Time.deltatime;
    16.  
    17.             if (key attack pressed)
    18.             {
    19.                 timeoutCombo = 0f;
    20.             }
    21.  
    22.             if (timeoutCombo > clearComboTimer)
    23.             {
    24.                 timeoutCombo = 0f;
    25.                 currentAttackCombo.Attacks.Clear();
    26.             }
    27.  
    28.             currentAttackCombo.Attacks.Add(currentState);
    29.             int comboIndex = ComboComplete();
    30.             if (comboIndex != -1)
    31.             {
    32.                 //Apply Combo
    33.                 int extraDmg = combos.Where(x => x.Index == comboIndex).FirstOrDefault().Index;
    34.                 currentAttackCombo.Attacks.Clear();
    35.             }
    36.             else
    37.             {
    38.                 //Apply normal damage
    39.             }
    40.         }
    41.  
    42.         int ComboComplete()
    43.         {
    44.             //some lambda expresion
    45.             var cmbList = combos.Where(x => x.Attacks.Count == currentAttackCombo.Attacks.Count);//only the one with same length
    46.             foreach (var cmb in cmbList)
    47.             {
    48.                 for (int i = 0; i < cmb.Attacks.Count; i++)
    49.                 {
    50.                     bool foundCombo = true;
    51.                     if (cmb.Attacks[i] != currentAttackCombo.Attacks[i])
    52.                     {
    53.                         foundCombo = false;
    54.                         break;
    55.                     }
    56.                     if (foundCombo)
    57.                     {
    58.                         return cmb.Index;
    59.                     }
    60.                 }
    61.             }
    62.          
    63.             return -1;
    64.         }
    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 :) but you need to modify the logic of this method.
     
    generalmcmutton likes this.
  3. generalmcmutton

    generalmcmutton

    Joined:
    May 20, 2010
    Posts:
    42
    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:


    Code (JavaScript):
    1. enum CharacterStates{idle, block, blockIn, blockOut, dodgeBack, dodgeForward, dodgeLeft, dodgeRight, duck, jump, slash1, slash2, slash3};
    2.  
    3. 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.
     
  4. adi7b9

    adi7b9

    Joined:
    Feb 22, 2015
    Posts:
    181
    Line81
    Code (CSharp):
    1. currentState = CharacterStates.slash2;
     
  5. generalmcmutton

    generalmcmutton

    Joined:
    May 20, 2010
    Posts:
    42
    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