HitState와 DeadState 작업 및 트러블슈팅

김보근·2024년 9월 12일

Unity

목록 보기
84/113

TIL: HitState와 DeadState 작업 및 트러블슈팅

HitState와 DeadState 구현

이번에 적에게 피격과 사망 상태를 부여하기 위해 HitState와 DeadState를 구현했다. State Machine을 이용해 적이 피격되었을 때 HitState로 전환하고, 체력이 0이 되었을 때 DeadState로 전환되도록 설계했다.

HitState: 적이 피격되면 피격 애니메이션이 재생되도록 하였고, 일정 시간 이후 다시 추적 상태로 돌아가게 했다.
DeadState: 체력이 0이 되면 사망 애니메이션이 재생되고, 애니메이션이 끝난 후 적을 비활성화하는 로직을 추가했다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour
{
    [SerializeField]
    public float speed; // 적의 속도
    public float health;
    public float maxHealth;

    public RuntimeAnimatorController[] animCon;

    public Rigidbody2D target; // 추적할 플레이어 또는 목표
    private Rigidbody2D rigid; // 적의 Rigidbody2D
    private SpriteRenderer sprite; // 스프라이트 제어

    private StateMachine stateMachine; // 상태 머신
    private Animator animator;
    private bool isLive; // 적이 살아 있는지 여부

    Collider2D coll;
    private void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
        sprite = GetComponent<SpriteRenderer>();
        animator = GetComponent<Animator>();
        coll = GetComponent<Collider2D>();

        // 상태 머신 초기화
        stateMachine = new StateMachine();
        // 게임이 시작되면 바로 플레이어를 추적하는 ChaseState로 전환
        //stateMachine.SetState(new ChaseState(stateMachine, animator, speed, rigid));
    }

    private void Update()
    {
        // 상태 머신 업데이트
        //if (isLive)
        //{
            Vector2 dirVec = target.position - rigid.position;
            stateMachine.Update(dirVec); // 플레이어 방향을 상태에 전달
        //}
    }

    private void LateUpdate()
    {
        // 적의 스프라이트 방향 처리
        sprite.flipX = target.position.x > rigid.position.x;
    }

    public void ChasePlayer()
    {
        if (isLive)
        {
            // ChaseState로 전환하여 플레이어 추적 시작
            stateMachine.SetState(new ChaseState(stateMachine, animator, speed, rigid));
        }
        
    }

    public void OnHit()
    {
        // 적이 피격되었을 때 피격 상태로 전환
        stateMachine.SetState(new HitState(stateMachine, animator, rigid));
    }

    private void OnEnable()
    {
        target = GameManager.Instance.player.GetComponent<Rigidbody2D>();
        isLive = true;
        coll.enabled = true;
        rigid.simulated = true;
        sprite.sortingOrder = 2;
        health = maxHealth;
    }

    public void Init(SpawnData data)
    {
        animator.runtimeAnimatorController = animCon[data.spriteType];
        speed = data.speed;
        maxHealth = data.health;
        health = data.health;
        ChasePlayer(); // 적이 활성화되면 바로 플레이어 추적 시작
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (!collision.CompareTag("Bullet"))
            return;

        health -= collision.GetComponent<Bullet>().damage;
        //StartCoroutine(KnockBack());

        if (health > 0)
        {
            OnHit();
        }
        else
        {
            isLive = false;
            coll.enabled = false;
            rigid.simulated = false;
            sprite.sortingOrder = 1;
            // DeadState로 전환
            stateMachine.SetState(new DeadState(stateMachine, animator, gameObject));
        }
    }
    //private void Dead()
    //{
    //    gameObject.SetActive(false);
    //}
}

HitState

using Unity.VisualScripting;
using UnityEngine;

public class HitState : Istate
{
    private StateMachine stateMachine;
    private Animator animator;
    private float knockbackDuration = 0.1f; // 피격 후 대기 시간
    private float elapsedTime;
    private Vector2 knockbackDirection;
    private Rigidbody2D rigid;

    public HitState(StateMachine stateMachine, Animator animator, Rigidbody2D rigid)
    {
        this.stateMachine = stateMachine;
        this.animator = animator;
        this.rigid = rigid;
    }

    public void Enter()
    {
        // 피격 애니메이션 재생
        animator.SetTrigger("Hit");
        elapsedTime = 0f;

        // knockback 처리
        Vector3 playerPos = GameManager.Instance.player.transform.position;
        Vector3 dirVec = rigid.position - (Vector2)playerPos;
        knockbackDirection = dirVec.normalized;
        rigid.AddForce(knockbackDirection * 1, ForceMode2D.Impulse);
    }

    public void Execute(Vector2 dirVec)
    {
        elapsedTime += Time.deltaTime;

        if (elapsedTime >= knockbackDuration)
        {
            // 피격 후 다시 추적 상태로 돌아감
            stateMachine.SetState(new ChaseState(stateMachine, animator, rigid.velocity.magnitude, rigid));
        }
    }

    public void Exit()
    {
        // 상태 종료 시 추가적인 작업이 필요하면 여기에 작성
    }


}

DeadState

using Unity.VisualScripting;
using UnityEngine;

public class DeadState : Istate
{
    private StateMachine stateMachine;
    private Animator animator;
    private GameObject enemy;
    private float deathAnimationDuration = 1f; // 사망 애니메이션 지속 시간
    private float elapsedTime;

    public DeadState(StateMachine stateMachine, Animator animator, GameObject enemy)
    {
        this.stateMachine = stateMachine;
        this.animator = animator;
        this.enemy = enemy;
    }

    public void Enter()
    {
        // 죽음 애니메이션을 재생
        animator.Play("Dead");
        elapsedTime = 0f;
    }

    public void Execute(Vector2 dirVec)
    {
        elapsedTime += Time.deltaTime;

        // Dead 애니메이션이 재생 중이고 끝났는지 확인
        if (animator.GetCurrentAnimatorStateInfo(0).IsName("Dead") &&
            animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1f)
        {
            Dead(); // 애니메이션이 끝나면 비활성화
        }
    }

    public void Exit()
    {
        // 상태가 끝날 때 추가 작업이 필요하면 여기에 작성
    }

    private void Dead()
    {
        // 적 비활성화
        enemy.SetActive(false);
    }
}

트러블슈팅: Execute 메서드가 호출되지 않았던 문제

Execute 메서드가 호출되지 않는 문제가 발생했었다. 원인은 상태 전환 후에도 상태 머신 업데이트가 isLive 플래그에 의해 막히고 있었기 때문이다. isLive가 false로 설정되면 상태 머신 업데이트가 멈추기 때문에 DeadState.Execute()가 호출되지 않았다.

이를 해결하기 위해 isLive 조건을 제거하고, 상태 머신이 계속해서 업데이트될 수 있도록 수정하였다. 그 후 DeadState에서 정상적으로 애니메이션이 끝나는 시점을 감지할 수 있었다.

private void Update()
{
    // 상태 머신 업데이트
    //if (isLive)
    //{
        Vector2 dirVec = target.position - rigid.position;
        stateMachine.Update(dirVec); // 플레이어 방향을 상태에 전달
    //}
}

사망 애니메이션이 불필요하게 남아있던 문제

처음에는 사망 애니메이션을 1초로 설정하고 타이머로 애니메이션이 끝나는 시점을 제어했었다. 하지만 이렇게 설정하니 애니메이션이 끝난 후에도 불필요하게 사망 애니메이션이 남아있는 현상이 발생했다.

이를 해결하기 위해 타이머 기반이 아닌 Animator의 상태 정보를 직접 확인하는 방식으로 변경했다. GetCurrentAnimatorStateInfo(0)normalizedTime을 활용하여 애니메이션이 정확히 끝났을 때 오브젝트를 비활성화하도록 처리했다. 이 방식은 더 유연하고 정확하여, 불필요하게 애니메이션이 남는 문제를 해결할 수 있었다.

이 방법의 장점:

애니메이션 길이에 상관없이 정확한 종료 시점을 감지할 수 있다.
애니메이션 상태에 맞춰 비활성화가 이루어져서 부드럽고 일관된 흐름을 유지할 수 있다.

public void Execute(Vector2 dirVec)
{
    elapsedTime += Time.deltaTime;

    // Dead 애니메이션이 재생 중이고 끝났는지 확인
    if (animator.GetCurrentAnimatorStateInfo(0).IsName("Dead") &&
        animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1f)
    {
        Dead(); // 애니메이션이 끝나면 비활성화
    }
}

요약

이번 작업을 통해 상태 전환에서 발생하는 문제를 해결하고, 타이머 대신 애니메이션 상태를 직접 확인하는 방식으로 더 정확한 사망 처리 로직을 구현할 수 있었다.

profile
게임개발자꿈나무

0개의 댓글