Enemy Movement Script not transitioning

CTA33

New member
Joined
Feb 19, 2025
Messages
1
Programming Experience
Beginner
my problem with this script i wrote, and ai edited/changed some, is that the enemy is supposed to switch from attacking to idle for a cooldown period and then back to attacking, while the player is in close proximity. but what happens is the cooldown fails, idle doesnt interchange with attacking, the enemy just hits and hits and hits in quick succession, and the animatiion is a little jittery. i have the proper animation events set. where is this script going wrong?? Thank you! spent many hours on this. I am a newbie. :)
C#:
using UnityEngine;

public class EnemyMovement : MonoBehaviour
{
// Public variables
public Rigidbody2D rb;
public float speed = 1f;
public float attackRange = 2f;
public float attackCoolDown = 2f;
public float playerDetectRange = 5f;
public Transform detectionPoint;
public LayerMask playerLayer;

// Private variables
private Transform player;
private int facingDirection = 1;
private Animator anim;
private EnemyState enemyState;
private float attackCoolDownTimer;
private bool isAttacking;

public enum EnemyState
{
    Idle,
    Chasing,
    Attacking,
    Patrolling
}

void Start()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();
    ChangeState(EnemyState.Idle);
}

void Update()
{
    CheckForPlayer();
    UpdateCooldownTimer();

    switch (enemyState)
    {
        case EnemyState.Chasing:
            Chase();
            break;
        case EnemyState.Attacking:
            if (!isAttacking)
            {
                rb.linearVelocity = Vector2.zero; // Stop movement when attacking
                Attack();
            }
            break;
        case EnemyState.Idle:
            if (attackCoolDownTimer <= 0 && player != null && Vector2.Distance(transform.position, player.position) <= attackRange)
            {
                ChangeState(EnemyState.Attacking);
            }
            break;
    }
}

private void UpdateCooldownTimer()
{
    if (attackCoolDownTimer > 0)
    {
        attackCoolDownTimer -= Time.deltaTime;
        Debug.Log($"Cooldown timer: {attackCoolDownTimer}");
    }
}

void Chase()
{
    if (player == null) return;

    if (Vector2.Distance(player.position, transform.position) <= attackRange)
    {
        ChangeState(EnemyState.Attacking);
    }
    else
    {
        AdjustFacingDirection();
        Vector2 direction = (player.position - transform.position).normalized;
        rb.linearVelocity = direction * speed; // Use linearVelocity instead of velocity
    }
}

private void AdjustFacingDirection()
{
    if (player.position.x > transform.position.x && facingDirection < 0 ||
        player.position.x < transform.position.x && facingDirection > 0)
    {
        Flip();
    }
}

void CheckForPlayer()
{
    Collider2D[] hits = Physics2D.OverlapCircleAll(detectionPoint.position, playerDetectRange, playerLayer);
    Debug.Log("Checking for player...");

    if (hits.Length > 0)
    {
        player = hits[0].transform;
        Debug.Log("Player detected!");

        if (Vector2.Distance(transform.position, player.position) <= attackRange && attackCoolDownTimer <= 0)
        {
            attackCoolDownTimer = attackCoolDown; // Reset cooldown when attacking
            ChangeState(EnemyState.Attacking);
        }
        else if (enemyState != EnemyState.Chasing)
        {
            ChangeState(EnemyState.Chasing);
        }
    }
    else
    {
        Debug.Log("No player detected.");
        if (enemyState != EnemyState.Idle)
        {
            ChangeState(EnemyState.Idle);
        }
    }
}

void Flip()
{
    facingDirection *= -1;
    transform.localScale = new Vector3(transform.localScale.x * -1, transform.localScale.y, transform.localScale.z);
}

public void Attack()
{
    // Implement attack logic here
    Debug.Log("Attack!");
    isAttacking = true;
    Invoke("OnAttackAnimationEnd", 1f); // Adjust the duration as per your attack animation length
}

public void OnAttackAnimationEnd()
{
    Debug.Log("Attack animation ended.");
    isAttacking = false;
    attackCoolDownTimer = attackCoolDown; // Reset the cooldown timer after the attack
    ChangeState(EnemyState.Idle); // Switch to Idle state after attack
}

void ChangeState(EnemyState newState)
{
    Debug.Log($"Changing state from {enemyState} to {newState}");
   
    // Exit the current animation
    switch (enemyState)
    {
        case EnemyState.Idle:
            anim.SetBool("isIdle", false);
            break;
        case EnemyState.Chasing:
            anim.SetBool("isChasing", false);
            break;
        case EnemyState.Attacking:
            anim.SetBool("isAttacking", false);
            break;
    }

    // Update our current state
    enemyState = newState;

    // Enter the new animation
    switch (enemyState)
    {
        case EnemyState.Idle:
            anim.SetBool("isIdle", true);
            rb.linearVelocity = Vector2.zero; // Ensure the enemy stops moving
            break;
        case EnemyState.Chasing:
            anim.SetBool("isChasing", true);
            break;
        case EnemyState.Attacking:
            anim.SetBool("isAttacking", true);
            rb.linearVelocity = Vector2.zero; // Ensure the enemy stops moving
            break;
    }
}


}
 
Last edited by a moderator:
Moving to 3rd party products subforum since we don't have a specific Unity area.
 
Your code is kind of convoluted, but I think what is happening is is that at time T, you are going into attack mode. On the next Update() call, line 102 in CheckForPlayer() is true because you were attacking last frame, and therefore still within attack range in this frame. Execution moves on to line 107. This is false because the cooldown timer is greater than 0, and so the check moves on to line 112. Since the current state is Attack, the state is then forced to be Chasing. When execution makes its way out of CheckForPlayer() back up to Update(), the switch statement on line 42 will go to line 44 since you are in a Chasing state. Line 45 will call Chase() which causes the state go back Attacking once more because of being in attack range. Since all of those state changes effectively make your animator to keep starting an new animation run.

At some point your OnAttackAnimationEnd() is called. The attack flag is turned off, the cool down timer is again reset to its max value, and the state is change to Idle. Update() will be called eventually, and once again, line 107 will be false, and line 112 will be true putting you into the Chase state. And then again, Chase() will put you into Attack state.
 
The problem with your script lies primarily in the state management logic, particularly in how the enemy transitions between states. The enemy should only switch to the attacking state if the cooldown timer has expired and the player is within range. However, the current implementation allows for immediate transitions back to attacking without respecting the cooldown.

To resolve this, ensure that the cooldown is enforced correctly. You can modify the Update method to check the cooldown before allowing a state change to attacking. Additionally, ensure that the OnAttackAnimationEnd method is called only after the attack animation completes, which you have implemented correctly with the Invoke method.

Make sure to reset the cooldown timer in the OnAttackAnimationEnd method, which you have done. However, ensure that the enemy does not switch back to attacking until the cooldown has fully elapsed. This will help maintain a proper attack rhythm and prevent jittery animations.

Lastly, consider adding a small delay before transitioning back to idle after an attack to allow for smoother animation transitions.

Here’s a refined version of the relevant section:
Refined Version:
case EnemyState.Idle:
    if (attackCoolDownTimer <= 0 && player != null && Vector2.Distance(transform.position, player.position) <= attackRange)
    {
        ChangeState(EnemyState.Attacking);
    }
    break;

Your code adjustment: For better functionality:
using UnityEngine;

public class EnemyMovement : MonoBehaviour
{
    // Public variables
    public Rigidbody2D rb;
    public float speed = 1f;
    public float attackRange = 2f;
    public float attackCooldown = 2f;
    public float playerDetectRange = 5f;
    public Transform detectionPoint;
    public LayerMask playerLayer;

    // Private variables
    private Transform player;
    private int facingDirection = 1;
    private Animator anim;
    private EnemyState enemyState;
    private float attackCooldownTimer;
    private bool isAttacking;

    public enum EnemyState
    {
        Idle,
        Chasing,
        Attacking,
        Patrolling
    }

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponent<Animator>();
        ChangeState(EnemyState.Idle);
    }

    void Update()
    {
        CheckForPlayer();
        UpdateCooldownTimer();

        switch (enemyState)
        {
            case EnemyState.Chasing:
                Chase();
                break;
            case EnemyState.Attacking:
                if (!isAttacking)
                {
                    rb.velocity = Vector2.zero; // Stop movement when attacking
                    Attack();
                }
                break;
            case EnemyState.Idle:
                if (attackCooldownTimer <= 0 && player != null && Vector2.Distance(transform.position, player.position) <= attackRange)
                {
                    ChangeState(EnemyState.Attacking);
                }
                break;
        }
    }

    private void UpdateCooldownTimer()
    {
        if (attackCooldownTimer > 0)
        {
            attackCooldownTimer -= Time.deltaTime;
            Debug.Log($"Cooldown timer: {attackCooldownTimer}");
        }
    }

    void Chase()
    {
        if (player == null) return;

        if (Vector2.Distance(player.position, transform.position) <= attackRange)
        {
            ChangeState(EnemyState.Attacking);
        }
        else
        {
            AdjustFacingDirection();
            Vector2 direction = (player.position - transform.position).normalized;
            rb.velocity = direction * speed; // Use velocity instead of linearVelocity
        }
    }

    private void AdjustFacingDirection()
    {
        if (player.position.x > transform.position.x && facingDirection < 0 ||
            player.position.x < transform.position.x && facingDirection > 0)
        {
            Flip();
        }
    }

    void CheckForPlayer()
    {
        Collider2D[] hits = Physics2D.OverlapCircleAll(detectionPoint.position, playerDetectRange, playerLayer);
        Debug.Log("Checking for player...");

        if (hits.Length > 0)
        {
            player = hits[0].transform;
            Debug.Log("Player detected!");

            if (Vector2.Distance(transform.position, player.position) <= attackRange && attackCooldownTimer <= 0)
            {
                attackCooldownTimer = attackCooldown; // Reset cooldown when attacking
                ChangeState(EnemyState.Attacking);
            }
            else if (enemyState != EnemyState.Chasing)
            {
                ChangeState(EnemyState.Chasing);
            }
        }
        else
        {
            Debug.Log("No player detected.");
            if (enemyState != EnemyState.Idle)
            {
                ChangeState(EnemyState.Idle);
            }
        }
    }

    void Flip()
    {
        facingDirection *= -1;
        transform.localScale = new Vector3(transform.localScale.x * -1, transform.localScale.y, transform.localScale.z);
    }

    public void Attack()
    {
        // Implement attack logic here
        Debug.Log("Attack!");
        isAttacking = true;
        Invoke("OnAttackAnimationEnd", 1f); // Adjust the duration as per your attack animation length
    }

    public void OnAttackAnimationEnd()
    {
        Debug.Log("Attack animation ended.");
        isAttacking = false;
        attackCooldownTimer = attackCooldown; // Reset the cooldown timer after the attack
        Invoke("ReturnToIdle", 0.5f); // Small delay before transitioning back to idle
    }

    private void ReturnToIdle()
    {
        if (attackCooldownTimer <= 0)
        {
            ChangeState(EnemyState.Idle); // Switch to Idle state after attack
        }
    }

    void ChangeState(EnemyState newState)
    {
        Debug.Log($"Changing state from {enemyState} to {newState}");

        // Exit the current animation
        switch (enemyState)
        {
            case EnemyState.Idle:
                anim.SetBool("isIdle", false);
                break;
            case EnemyState.Chasing:
                anim.SetBool("isChasing", false);
                break;
            case EnemyState.Attacking:
                anim.SetBool("isAttacking", false);
                break;
        }

        // Update our current state
        enemyState = newState;

        // Enter the new animation
        switch (enemyState)
        {
            case EnemyState.Idle:
                anim.SetBool("isIdle", true);
                rb.velocity = Vector2.zero; // Ensure the enemy stops moving
                break;
            case EnemyState.Chasing:
                anim.SetBool("isChasing", true);
                break;
            case EnemyState.Attacking:
                anim.SetBool("isAttacking", true);
                rb.velocity = Vector2.zero; // Ensure the enemy stops moving
                break;
        }
    }
}
 
Last edited:
Back
Top Bottom