Forum begins after the advertisement:
[Part6] Enemies Animation question
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part6] Enemies Animation question
- This topic has 8 replies, 4 voices, and was last updated 1 week, 2 days ago by Terence.
-
AuthorPosts
-
November 15, 2024 at 4:59 pm #16348::
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
November 19, 2024 at 12:19 pm #16415November 27, 2024 at 8:04 pm #16568::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
-
Create a Bool Parameter in Animator:
- Open your Animator window and add a new parameter, e.g.,
isAttacking
.
- Open your Animator window and add a new parameter, e.g.,
-
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; }
-
In Animator:
- Set transitions from
Attack
back toIdle
only whenisAttacking
isfalse
.
- Set transitions from
Option 2: Use Animation Events
-
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
.
-
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; }
-
In Animator:
- Ensure the attack state transitions only when
isAttacking
isfalse
.
- Ensure the attack state transitions only when
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
toIdle
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.
November 29, 2024 at 1:50 pm #16604::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); } */ }
November 29, 2024 at 11:50 pm #16629::To make sure your attack animation plays fully, set the Exit Time to 1 on all transitions away from it.
December 1, 2024 at 11:38 am #16636::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); } */ }
December 1, 2024 at 3:10 pm #16638::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.December 1, 2024 at 6:54 pm #16643::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
December 2, 2024 at 12:10 pm #16648::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. } }
-
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: