Forum begins after the advertisement:


[Part6] Enemies Animation question

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part6] Enemies Animation question

Viewing 9 posts - 1 through 9 (of 9 total)
  • Author
    Posts
  • #16348
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    This article was translated using Google. Please forgive me if the wording is wrong.

    Hi, I recently came back to make this game. I encountered a little problem in making new monsters. I found that the monsters made in the video all cause damage through collision with the player. So I made a farther attack animation for my monster, which is probably a frog sticking out its tongue to attack, but I encountered a problem. My monster will trigger the animation when it enters the attack range, but when the animation is turned halfway, if When the player leaves the attack range, the monster will return to the Idle state. Is there any way to force it to play the full animation before returning to the Idle state or other states?

    If you need a video or code for my current monster please let me know

    #16415
    Chloe Lim
    Level 10
    Moderator
    Helpful?
    Up
    0
    ::

    I can try to help, can I see the code for this enemy?

    #16568
    A_DONUT
    Level 6
    Moderator
    Helpful?
    Up
    0
    ::

    pls give more info but still i will try to solve ur doubt To ensure your monster’s attack animation plays fully before switching back to the idle state or any other state, you can use an animation event system or control the state transitions via Animator parameters and a coroutine. Here’s how you can approach this:


    Option 1: Control Transitions with Animator Parameters

    1. Create a Bool Parameter in Animator:

      • Open your Animator window and add a new parameter, e.g., isAttacking.
    2. Modify Your Code: Ensure your monster doesn’t exit the attack state until the animation finishes:

      private Animator animator;
      private bool isAttacking = false;
      
      void Start()
      {
          animator = GetComponent<Animator>();
      }
      
      void Update()
      {
          if (!isAttacking && PlayerInAttackRange())  // Your condition for entering attack
          {
              StartCoroutine(AttackCoroutine());
          }
      }
      
      IEnumerator AttackCoroutine()
      {
          isAttacking = true;
          animator.SetBool("isAttacking", true);  // Triggers attack animation
      
          // Wait until the current attack animation finishes
          yield return new WaitForSeconds(animator.GetCurrentAnimatorStateInfo(0).length);
      
          animator.SetBool("isAttacking", false);
          isAttacking = false;
      }
    3. In Animator:

      • Set transitions from Attack back to Idle only when isAttacking is false.

    Option 2: Use Animation Events

    1. Add an Event to the Animation:

      • In Unity, select your attack animation.
      • Open the Animation window and click on the frame where you want the attack to finish.
      • Click “Add Event” and select a method, such as OnAttackComplete.
    2. Create the Method in Your Script:

      private bool isAttacking = false;
      
      void Update()
      {
          if (!isAttacking && PlayerInAttackRange())
          {
              animator.SetTrigger("Attack");  // Triggers attack animation
              isAttacking = true;
          }
      }
      
      // Called by the animation event at the end of the attack
      public void OnAttackComplete()
      {
          isAttacking = false;
      }
    3. In Animator:

      • Ensure the attack state transitions only when isAttacking is false.

    Option 3: Prevent Interruptions Using AnimatorStateInfo

    You can prevent other states from interrupting the attack by checking if the current state is still playing:

    void Update()
    {
        if (PlayerInAttackRange() && !isAttacking)
        {
            animator.SetTrigger("Attack");
            isAttacking = true;
        }
    
        // Check if the attack animation has finished playing
        if (isAttacking && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1 && animator.GetCurrentAnimatorStateInfo(0).IsName("Attack"))
        {
            isAttacking = false;
        }
    }

    Tips for Smooth Transitions:

    • Ensure Transitions in Animator are Properly Set: Check that transitions from Attack to Idle have an appropriate condition (e.g., isAttacking == false).
    • Disable Exit Time on Transitions: This will prevent premature transitions before the animation is complete.

    By managing the state machine through these methods, you’ll ensure the full attack animation plays before returning to idle or other states, enhancing the gameplay experience.

    #16604
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    I forgot to turn on email reminders, so I only saw the reply now, I’m sorry I modified it from the monster in the tutorial video. I added an attack state, but I don’t know how to play the complete attack animation and use the above tutorial in the state machine.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Frog : Enemy
    {
        float timer;
        [SerializeField] private float flipWaitTime;
        [SerializeField] private float ledgeCheckX;
        [SerializeField] private float ledgeCheckY;
        [SerializeField] private LayerMask WhatIsGround;
    
        [SerializeField] private Transform attackTransform;
        [SerializeField] private float attackDistance;
    
    
    
        protected override void Start()
        {
            base.Start();
            rb.gravityScale = 12f;
        }
    
    
        protected override void Update()
        {
            base.Update();
            if(!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Frog_Idle);
            }
    
        }
    
        private void OnCollisionEnter2D(Collision2D _collision)
    {
            if(_collision.gameObject.CompareTag("Monster"))
            {
                ChangeState(EnemyStates.Frog_Flip);
            }
        }
    
    
        protected override void UpdateEnemyStates()
        {
            if(health <= 0)
            {
                Death(0.05f);
            }
            //玩家與該怪物的距離
            float _dist = Vector2.Distance(attackTransform.position, PlayerController.Instance.transform.position);
    
            switch (GetCurrentEnemyState)
            {
                case EnemyStates.Frog_Idle://正常狀態
    
                    Vector3 _ledgiCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
                    Vector2 _wallCheckDir = transform.localScale.x > 0 ? transform.right : -transform.right;
    
                    if(_dist < attackDistance)
                    {
                        ChangeState(EnemyStates.Frog_Attack);
                    }
    
                    if (!Physics2D.Raycast(transform.position + _ledgiCheckStart, Vector2.down, ledgeCheckY, WhatIsGround)
                        || Physics2D.Raycast(transform.position, _wallCheckDir, ledgeCheckX, WhatIsGround))
                    {
                        ChangeState(EnemyStates.Frog_Flip);
                    }
    
                    if (transform.localScale.x > 0)
                    {
                        rb.velocity = new Vector2(speed, rb.velocity.y);
                    }
                    else
                    {
                        rb.velocity = new Vector2(-speed, rb.velocity.y);
                    }
                    break;
    
                case EnemyStates.Frog_Attack:
    
                        if (_dist > attackDistance)
                        {
                            ChangeState(EnemyStates.Frog_Idle);
                        }
                    break;
    
                case EnemyStates.Frog_Flip:
                    timer += Time.deltaTime;
    
                    if(timer > flipWaitTime)
                    {
                        timer = 0;
                        transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y);
                        ChangeState(EnemyStates.Frog_Idle);
                    }
                    break; 
            }
        }
    
        protected override void ChangeCurrentAnimation() //動畫切換
        {
            anim.SetBool("Idle", GetCurrentEnemyState == EnemyStates.Frog_Idle);
    
            anim.SetBool("Attack", GetCurrentEnemyState == EnemyStates.Frog_Attack);
    
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(attackTransform.position, attackDistance);
        }
    
        /*
        public override void EnemyHit(float _DamageDone, Vector2 _hitDirection, float _hitForce)
        {
            base.EnemyHit(_DamageDone, _hitDirection, _hitForce);
        }
        */
    }
    #16629
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    To make sure your attack animation plays fully, set the Exit Time to 1 on all transitions away from it.

    #16636
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    It worked. But I have a problem now. I set an event Attack() in my attack animation, but this causes the player to be attacked no matter how far away or at any position. How should I make the player attack when I set it? Only when it is detected in a certain boxCollider will it be attacked.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Enemy : MonoBehaviour
    {
        [SerializeField] protected float health;
        [SerializeField] protected float recoilLength; //反衝暫停時間
        [SerializeField] protected float recoilFactor;
        [SerializeField] protected bool isRecoiling = false; //是否反衝
    
        [SerializeField] protected PlayerController player;
    
        [SerializeField] protected float speed; //速度
    
        [SerializeField] protected float damage; //傷害
        [SerializeField] protected GameObject hitEffect; //受到傷害效果
    
        [SerializeField] protected GameObject AttackArea; //攻擊範圍
    
    
    
        protected float recoilTimer;
        protected Rigidbody2D rb;
        protected SpriteRenderer sr;
        protected Animator anim;
    
        protected enum EnemyStates
        {
            //Frog
            Frog_Idle,
            Frog_Flip,
            Frog_Attack,
    
            //Bat
            Bat_Idle,
            Bat_Chase,
            Bat_Stunned,
            Bat_Death,
    
            //Charger
            Charger_Idle,
            Charger_Suprised,
            Charger_Charge,
        }
    
        protected EnemyStates currentEnemyStates;
    
        protected virtual EnemyStates GetCurrentEnemyState //檢測敵人狀態發生改變
        {
            get { return currentEnemyStates; }
            set
            {
                if(currentEnemyStates != value)
                {
                    currentEnemyStates = value;
    
                    ChangeCurrentAnimation();
                }
            }
        }
    
        protected virtual void Start()
        {
    
        }
    
        protected virtual void Awake()
        {
            rb = GetComponent<Rigidbody2D>();
    
            sr = GetComponent<SpriteRenderer>();
    
            anim = GetComponent<Animator>();
    
            player = PlayerController.Instance;
    
        }
    
        protected virtual void Update()
        {
            if (GameManager.Instance.isPaused) return;
    
            if(isRecoiling)
            {
                if(recoilTimer < recoilLength)
                {
                    recoilTimer += Time.deltaTime;
                }
                else
                {
                    isRecoiling = false;
                    recoilTimer = 0;
                }
            }
            else
            {
                UpdateEnemyStates();
            }
        }
    
        public virtual void EnemyHit(float _DamageDone, Vector2 _hitDirection, float _hitForce)
        {
            health -= _DamageDone;
            if(!isRecoiling)
            {
                GameObject _hitEffect = Instantiate(hitEffect, transform.position, Quaternion.identity); //實例化受傷效果
                Destroy(_hitEffect, 1.0f); //刪除物件
    
                rb.velocity = _hitForce * recoilFactor * _hitDirection;
                isRecoiling = true;
            }
        }
    
        protected virtual void OnCollisionStay2D (Collision2D _other)
        {
            if(_other.gameObject.CompareTag("Player") && !PlayerController.Instance.pState.invincible && health > 0)
            {
                Attack();
            }
        }
    
    
        protected virtual void Death(float _destroyTime)
        {
            Destroy(gameObject, _destroyTime);
        }
    
        protected virtual void UpdateEnemyStates()
        {
    
        }
    
        protected virtual void ChangeCurrentAnimation()
        {
    
        }
    
        protected void ChangeState(EnemyStates _newState)
        {
            GetCurrentEnemyState = _newState;
        }
    
        protected virtual void Attack()
        {
            PlayerController.Instance.TakeDamage(damage);
    
            if (PlayerController.Instance.pState.alive)
            {
                //PlayerController.Instance.HitStopTime(0, 5, 0.5f); //時間刻度為0 恢復速度為5 延遲設定為0.5
                GameManager.Stop();
            }
        }
    }
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Frog : Enemy
    {
        float timer;
        [SerializeField] private float flipWaitTime;
        [SerializeField] private float ledgeCheckX;
        [SerializeField] private float ledgeCheckY;
        [SerializeField] private LayerMask WhatIsGround;
    
        [SerializeField] private Transform attackTransform;
        [SerializeField] private float attackDistance;
    
    
    
        protected override void Start()
        {
            base.Start();
            rb.gravityScale = 12f;
        }
    
    
        protected override void Update()
        {
            base.Update();
            if(!PlayerController.Instance.pState.alive)
            {
                ChangeState(EnemyStates.Frog_Idle);
            }
    
        }
    
        private void OnCollisionEnter2D(Collision2D _collision)
        {
            if(_collision.gameObject.CompareTag("Monster"))
            {
                ChangeState(EnemyStates.Frog_Flip);
            }
        }
    
    
        protected override void UpdateEnemyStates()
        {
            if(health <= 0)
            {
                Death(0.05f);
            }
            //玩家與該怪物的距離
            float _dist = Vector2.Distance(attackTransform.position, PlayerController.Instance.transform.position);
    
            switch (GetCurrentEnemyState)
            {
                case EnemyStates.Frog_Idle://正常狀態
    
                    Vector3 _ledgiCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0);
                    Vector2 _wallCheckDir = transform.localScale.x > 0 ? transform.right : -transform.right;
    
                    if(_dist < attackDistance)
                    {
                        ChangeState(EnemyStates.Frog_Attack);
                    }
    
                    if (!Physics2D.Raycast(transform.position + _ledgiCheckStart, Vector2.down, ledgeCheckY, WhatIsGround)
                        || Physics2D.Raycast(transform.position, _wallCheckDir, ledgeCheckX, WhatIsGround))
                    {
                        ChangeState(EnemyStates.Frog_Flip);
                    }
    
                    if (transform.localScale.x > 0)
                    {
                        rb.velocity = new Vector2(speed, rb.velocity.y);
                    }
                    else
                    {
                        rb.velocity = new Vector2(-speed, rb.velocity.y);
                    }
                    break;
    
                case EnemyStates.Frog_Attack:
    
                        if (_dist > attackDistance)
                        {
                            ChangeState(EnemyStates.Frog_Idle);
                        }
                    break;
    
                case EnemyStates.Frog_Flip:
                    timer += Time.deltaTime;
    
                    if(timer > flipWaitTime)
                    {
                        timer = 0;
                        transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y);
                        ChangeState(EnemyStates.Frog_Idle);
                    }
                    break; 
            }
        }
    
        protected override void ChangeCurrentAnimation() //動畫切換
        {
            anim.SetBool("Idle", GetCurrentEnemyState == EnemyStates.Frog_Idle);
    
            anim.SetBool("Attack", GetCurrentEnemyState == EnemyStates.Frog_Attack);
    
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(attackTransform.position, attackDistance);
        }
    
        /*
        public override void EnemyHit(float _DamageDone, Vector2 _hitDirection, float _hitForce)
        {
            base.EnemyHit(_DamageDone, _hitDirection, _hitForce);
        }
        */
    }
    #16638
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Making the detection only happen when in a certain box collider is tricky, beacuse there are usually multiple colliders on a single GameObject, which makes things messy.

    A cleaner way is to check the distance before attacking. Something like this:

    
    protected virtual void OnCollisionStay2D (Collision2D _other)
    {
        bool withinDistance = Vector2.Distance(_other.transform.position, transform.position) < 1f;
        if(withinDistance && _other.gameObject.CompareTag("Player") && !PlayerController.Instance.pState.invincible && health > 0)
        {
            Attack();
        }
    }
    

    Of course, you need to replace the 1f above with your detection range.

    #16643
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    I’m wondering if there are some errors in using the detection distance method. For example: I happened to jump up, but I was actually not within the monster’s attack range, but because the distance to the monster was smaller than the detection distance, I was still attacked.

    My idea is that I add a sub-object (attack range) under the monster, and then put BoxCollider2D and set it to isTrigger, I’m wondering if there’s a way to set it up as a function that can detect if the player is within attack range, and then when the animation reaches a specific frame Call this event. But I don’t know if this idea can be realized

    #16648
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Yes @mini, you can do something like this without colliders:

    public float acquisitionRange = 1f;
    bool isInRange = false;
    void Update() {
        // Use isInRange to check if you can attack.
        isInRange = Vector2.Distance(transform.position, player.transform.position) < acquisitionRange;
    
        // If player is in range then attack.
        if(isInRange) {
            // Do your attack.
        }
    }
Viewing 9 posts - 1 through 9 (of 9 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: