Search Unity

  1. Looking for a job or to hire someone for a project? Check out the re-opened job forums.
    Dismiss Notice
  2. Unity 2020 LTS & Unity 2021.1 have been released.
    Dismiss Notice
  3. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Resolved Enum state function fires in another state

Discussion in '2D' started by rizenmusic, Mar 1, 2021.

  1. rizenmusic

    rizenmusic

    Joined:
    Oct 2, 2019
    Posts:
    22
    Hi! I'm trying to implement a "lazy state machine" using enums for simple enemy AI but for some reason the logic doesn't work as intended. I need my enemy to stop from time to time to be in Idle state. But when Idle switch fires, my enemy continues to move despite movement requiring another state to fire.

    What am I doing wrong?

    Code (CSharp):
    1.  private void Update()
    2.     {
    3.         Debug.Log(state);
    4.         onGround = Physics2D.Raycast(gameObject.transform.position, Vector2.down, groundLength, groundLayer);
    5.         Debug.DrawRay(gameObject.transform.position, Vector2.down * 1f, Color.red);
    6.  
    7.         isAnythingThere = Physics2D.Raycast(wallDetection.position, Vector2.right * speed, 0.3f, sideInfo);
    8.         RaycastHit2D groundInfo = Physics2D.Raycast(groundDetection.position, Vector2.down, distance);
    9.         RaycastHit2D isPlayerInSight = Physics2D.Raycast(wallDetection.position, Vector2.right * speed, 3f, playerLayer);
    10.  
    11.         switch (state)
    12.         {
    13.             default:
    14.             case State.patrol:
    15.  
    16.                 if ((groundInfo.collider == false || isAnythingThere) && !isPlayerInSight)
    17.                 {
    18.                     if (movingRight == true)
    19.                     {
    20.                         Debug.Log("Turn");
    21.                         transform.eulerAngles = new Vector3(0, -180, 0);
    22.                         movingRight = false;
    23.                         speed = -speed;
    24.                     }
    25.                     else
    26.                     {
    27.                         transform.eulerAngles = new Vector3(0, 0, 0);
    28.                         movingRight = true;
    29.                         speed = -speed;
    30.                     }
    31.                 }
    32.                 if (isPlayerInSight)
    33.                 {
    34.                     state = State.attack;
    35.                 }
    36.                 if (enemyIdleCheck == false)
    37.                 {
    38.                     StartCoroutine("IfEnemyWantsToIdle");
    39.                 }
    40.                 break;
    41.  
    42.             case State.attack:
    43.  
    44.                 RaycastHit2D isPlayerThere = Physics2D.Raycast(wallDetection.position, Vector2.right * speed, 3f, playerLayer);
    45.                 Debug.DrawRay(wallDetection.transform.position, Vector2.right * speed * 3f, Color.red);
    46.                 if (isPlayerThere)
    47.                 {
    48.                     attackDirection = (playerPosition.position - transform.position).normalized;
    49.                 }
    50.                 else
    51.                 {
    52.                     if (!jumpCooldown)
    53.                     {
    54.                         state = State.patrol;
    55.                     }
    56.                 }
    57.                 if (jumpCooldown == false)
    58.                 {
    59.                     jumpTimer = Time.time + jumpCooldownInSeconds;
    60.                 }
    61.                 break;
    62.  
    63.             case State.idle:
    64.  
    65.                 if (isPlayerInSight)
    66.                 {
    67.                     state = State.attack;
    68.                 }
    69.                 if (!isInIdleState)
    70.                 {
    71.                     StartCoroutine("IdlingTime");
    72.                 }
    73.                 break;
    74.         }
    75.     }
    76.  
    77.     private IEnumerator IfEnemyWantsToIdle()
    78.     {
    79.         enemyIdleCheck = true;
    80.         Debug.Log("Checking for idle possibility");
    81.         yield return new WaitForSeconds(2);
    82.         if (Random.Range(0, 20) > 10)
    83.         {
    84.             state = State.idle;
    85.         }
    86.         enemyIdleCheck = false;
    87.  
    88.         if (isPlayerInSight)
    89.         {
    90.             state = State.attack;
    91.             enemyIdleCheck = false;
    92.             yield break;
    93.         }
    94.     }
    95.  
    96.     private IEnumerator IdlingTime()
    97.     {
    98.         isInIdleState = true;
    99.         while (state == State.idle)
    100.         {
    101.             yield return new WaitForSeconds(Random.Range(2, 4));
    102.             isInIdleState = false;
    103.             state = State.patrol;
    104.         }
    105.         if (isPlayerInSight)
    106.         {
    107.             isInIdleState = false;
    108.             yield break;
    109.         }
    110.     }
    111.  
    112.     private void FixedUpdate()
    113.     {
    114.         if (state == State.patrol)
    115.         {
    116.             if (!enemy.isTakingDamage)
    117.             {
    118.                 rb.velocity = new Vector2(movingSpeed * speed, rb.velocity.y);
    119.             }
    120.         }
    121.  
    122.         if (state == State.attack)
    123.         {
    124.             if (!enemy.isTakingDamage)
    125.             {
    126.                 rb.velocity = new Vector2(attackDirection.x * attackSpeed, rb.velocity.y);
    127.             }
    128.  
    129.             if ((Random.Range(0, 100) > 90) && !jumpCooldown && onGround)
    130.             {
    131.                 Debug.Log("Jumped!");
    132.                 //rb.velocity = new Vector2(rb.velocity.x, rb.velocity.y * jumpForce);
    133.                 rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
    134.                 jumpCooldown = true;
    135.                 Invoke("JumpCooldownTime", 1f);
    136.             }
    137.         }
    138.     }
     
  2. Cornysam

    Cornysam

    Joined:
    Feb 8, 2018
    Posts:
    762
    So you have your switch statement in Update and then you also have state switching in FixedUpdate. Update is happening every frame and Fixed is happening almost every frame so i am guessing you have some conflicting code but its hard to tell at first glance.

    I suggest changing up your FSM a bit, here is what works well for me:

    Code (CSharp):
    1.  
    2.     public enum EnemyActionType { Idle = 0, Patrol = 1, Attack = 2};
    3.     public EnemyActionType CurrentState = EnemyActionType.Idle;
    4.    
    5.     private void Start()
    6.     {
    7.         ChangeState(CurrentState);
    8.     }
    9.    
    10.     public void ChangeState(EnemyActionType NewState)
    11.     {
    12.         StopAllCoroutines();
    13.         CurrentState = NewState;
    14.  
    15.         switch (NewState)
    16.         {
    17.             case EnemyActionType.Idle:
    18.                 StartCoroutine(Idle());
    19.                 break;
    20.  
    21.             case EnemyActionType.Patrol:
    22.                 StartCoroutine(Patrol());
    23.                 break;
    24.  
    25.             case EnemyActionType.Attack:
    26.                 StartCoroutine(Attack());
    27.                 break;
    28.         }
    29.     }
    30.    
    31.     public IEnumerator Patrol()
    32.     {
    33.         while(CurrentState == EnemyActionType.Patrol)
    34.         {
    35.             //Do my raycast here
    36.             if(some condition)
    37.             {
    38.                 ChangeState(EnemyActionType.Attack);
    39.             }
    40.             if(some other condition)
    41.             {
    42.                 ChangeState(EnemyActionType.Idle);
    43.             }
    44.             yield return null;
    45.         }
    46.     }
    47.    
    48.     public IEnumerator Idle()
    49.     {
    50.         while(CurrentState == EnemyActionType.Idle)
    51.         {
    52.             //Do my raycast here
    53.             if(some condition)
    54.             {
    55.                 ChangeState(EnemyActionType.Attack);
    56.             }
    57.             if(some other condition)
    58.             {
    59.                 ChangeState(EnemyActionType.Patrol);
    60.             }
    61.             yield return null;
    62.         }
    63.     }
    64.    
    65.     public IEnumerator Attack()
    66.     {
    67.         while(CurrentState == EnemyActionType.Attack)
    68.         {
    69.             //Do my raycast here
    70.             if(some condition)
    71.             {
    72.                 ChangeState(EnemyActionType.Patrol);
    73.             }
    74.             if(some other condition)
    75.             {
    76.                 ChangeState(EnemyActionType.Idle);
    77.             }
    78.             yield return null;
    79.         }
    80.     }
    81.        
    82.    
    83.     private void FixedUpdate()
    84.     {
    85.         if (turning == true && !facingRight)
    86.             Flip();
    87.         else if (turning == false && facingRight)
    88.             Flip();
    89.     }
    90.  
    91.     void Flip()
    92.     {
    93.         facingRight = !facingRight;
    94.         Vector3 theScale = transform.localScale;
    95.         theScale.x *= -1;
    96.         transform.localScale = theScale;
    97.     }
    You will notice that I have to redo my Raycast every state. There is probably a way around this but I do it so I dont have 3 hit variables in FixedUpdate or Update and then have to use 3 if statements in each State action.

    But, essentially you are removing all Update and FixedUpdate state switches and making them dependent on each other. That is, the actual switching of states is handled by the function ChangeState. You then call that ChangeState whenever a condition is met within your states.

    For my idle, I set up a basic timer. If the raycast is null and the timer has expired, go to patrol state. If the patrol state is active, it does the same thing: Go to 2 or 3 patrol points if raycast is null, then wait some seconds before going back to idle. If the raycast hits the player, switch to attack state, etc. etc. etc.
     
    rizenmusic likes this.
  3. rizenmusic

    rizenmusic

    Joined:
    Oct 2, 2019
    Posts:
    22
    @Cornysam thanks, interesting solution. I was thinking about making my FSM based on Coroutines before though I'm not sure if it would be ideal to put physics and state logic in a coroutine together. Looks like I'll have to implement a full scale abstract state machine eventually)
    What was the solution for my problem - the velocity in my script was constant so I had to null it in another FixedUpdate state:

    Code (CSharp):
    1. case State.idle:
    2.                 rb.velocity = Vector2.zero;
    3.                 break;
    It's working fine now.
     
  4. Cornysam

    Cornysam

    Joined:
    Feb 8, 2018
    Posts:
    762
    Glad you got it working. I made that script like 2 and a half years ago when i was first learning about basic AI enemies and got it working so i havent really changed it since. So far, no issues with physics and state logic together but maybe ill find out some day haha
     
unityunity