본문 바로가기

유니티(몬스터 행동패턴 (Finite State Machine, 유한 상태 머신) 만들기 ) _ 멋쟁이사자처럼 유니티 부트캠프 후기 32회차

@salmu2025. 7. 5. 18:57
반응형

[32회차 수업내용] 250701

1. DontDestroyOnLoad

2. MosterCore로 추상클래스, Goblin Idle, Patrol, Trace, Attack 정의

 

 

1. DontDestroyOnLoad

: 이 코드를 쓰면 gameObject가 씬 전환 시에도 파괴되지 않음 

: 사운드 컨트롤러에 추가 (Awake에)

 

 

2. 몬스터 FSM

* FSM(Finite State Machine) = “지금 내가 어떤 상태인지” + “상태 간 전환 규칙”

 

2.1 모든 몬스터의 기본 동작을 정의하는 추상 클래스 

// 추상 몬스터 코어 클래스: 공통 속성과 상태(FSM) 로직을 정의
public abstract class MonsterCore : MonoBehaviour
{
    // 몬스터 상태 정의 (유한 상태 머신 - FSM)
    public enum MonsterState { IDLE, PATROL, TRACE, ATTACK }

    // 현재 몬스터 상태 (기본값은 IDLE)
    public MonsterState monsterState = MonsterState.IDLE;

    // 체력과 속도 (외부에서 Init으로 설정)
    public float hp;
    public float speed;

    // 초기 설정 (자식 클래스에서 호출)
    protected virtual void Init(float hp, float speed)
    {
        this.hp = hp;
        this.speed = speed;
    }

    // 상태에 따라 매 프레임 행동 분기
    void Update()
    {
        switch (monsterState)
        {
            case MonsterState.IDLE:
                Idle();    // 대기 상태
                break;
            case MonsterState.PATROL:
                Patrol();  // 순찰 상태
                break;
            case MonsterState.TRACE:
                Trace();   // 추적 상태
                break;
            case MonsterState.ATTACK:
                Attack();  // 공격 상태
                break;
        }
    }

    // 각 상태별 행동은 자식 클래스에서 구현해야 함
    public abstract void Idle();
    public abstract void Patrol();
    public abstract void Trace();
    public abstract void Attack();
}

*리마인드

protected 자신 + 자식만 접근 가능
virtual 오버라이딩 가능하게 허용
override 부모의 virtual 메서드를 재정의

 

 

 

 

 

2.2 애니메이터설정

  • 고블린 애니메이터
    • 부모 클래스: MonsterCore 공통된 동작과 상태 관리 담당
    • 자식 클래스: Goblin 몬스터 개별 행동 세부 구현 담당

 

2.3 코드 해석

-부모클래스 (전체코드)

// 몬스터의 기본 동작과 상태를 정의하는 추상 클래스 (기반 뼈대)
public abstract class MonsterCore : MonoBehaviour
{
    // 몬스터 행동 상태를 표현하는 열거형 (FSM의 상태들)
    public enum MonsterState { IDLE, PATROL, TRACE, ATTACK }

    // 현재 몬스터 상태, 기본값은 IDLE
    public MonsterState monsterState = MonsterState.IDLE;

    // 몬스터 공통 컴포넌트들 (자식 클래스 접근 가능하도록 protected)
    protected Animator animator;          // 애니메이션 조작용 컴포넌트
    protected Rigidbody2D monsterRb;      // 물리적 움직임 처리용 Rigidbody2D
    protected Collider2D monsterColl;     // 충돌 감지용 Collider2D

    // 추적 대상 (주로 플레이어 Transform)
    public Transform target;

    // 몬스터 스탯 (체력, 이동 속도, 공격 간격)
    public float hp, speed, attackTime;

    // 몬스터 상태 관련 정보
    protected float moveDir;              // 이동 방향 (1 또는 -1)
    protected float targetDist;           // 플레이어와의 거리
    protected bool isTrace;               // 플레이어 추적 여부 플래그

    // 초기화 함수: 스탯 설정, 주요 컴포넌트 연결, 플레이어 자동 할당
    protected virtual void Init(float hp, float speed, float attackTime)
    {
        this.hp = hp;                     // 체력 초기화
        this.speed = speed;               // 이동 속도 초기화
        this.attackTime = attackTime;    // 공격 쿨타임 초기화

        // 플레이어 태그로 자동 탐색 후 Transform 할당
        target = GameObject.FindGameObjectWithTag("Player").transform;

        // 현재 게임 오브젝트에서 컴포넌트들 가져오기 (Animator, Rigidbody2D, Collider2D)
        animator = GetComponent<Animator>();
        monsterRb = GetComponent<Rigidbody2D>();
        monsterColl = GetComponent<Collider2D>();
    }

    // 매 프레임 실행되는 업데이트 함수
    void Update()
    {
        // 플레이어와 몬스터 간 거리 계산
        targetDist = Vector3.Distance(transform.position, target.position);

        // 몬스터가 바라보는 방향 벡터 (moveDir 방향)
        Vector3 monsterDir = Vector3.right * moveDir;

        // 몬스터에서 플레이어 방향으로 향하는 벡터 (정규화)
        Vector3 playerDir = (transform.position - target.position).normalized;

        // 두 벡터 간 내적 계산 (cosine 유사도)
        float dotValue = Vector3.Dot(monsterDir, playerDir);

        // dotValue가 -0.5 ~ -1 이면, 몬스터가 플레이어를 향해 바라보고 있다고 판단
        isTrace = dotValue < -0.5f && dotValue >= -1f;

        // 현재 상태에 따라 해당 상태 함수 호출 (FSM 실행)
        switch (monsterState)
        {
            case MonsterState.IDLE:
                Idle();     // 대기 상태 행동
                break;
            case MonsterState.PATROL:
                Patrol();   // 순찰 상태 행동
                break;
            case MonsterState.TRACE:
                Trace();    // 플레이어 추적 행동
                break;
            case MonsterState.ATTACK:
                Attack();   // 공격 행동
                break;
        }
    }

    // 벽 또는 경계 오브젝트(태그 "Return")에 닿으면 방향 전환 처리
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Return"))
        {
            moveDir *= -1;  // 이동 방향 반전
            // 스프라이트 좌우 반전 처리 (moveDir에 따라 x축 크기 변경)
            transform.localScale = new Vector3(moveDir, 1, 1);
        }
    }

    // 몬스터 상태 변경 함수: 중복 상태 변경 방지
    public void ChangeState(MonsterState newState)
    {
        if (monsterState != newState)
            monsterState = newState;
    }

    // 상태별 행동은 자식 클래스에서 반드시 구현해야 하는 추상 메서드들
    public abstract void Idle();     // 대기 상태 행동 정의
    public abstract void Patrol();   // 순찰 상태 행동 정의
    public abstract void Trace();    // 추적 상태 행동 정의
    public abstract void Attack();   // 공격 상태 행동 정의
}

 

 

-자식클래스(고블린전체코드)

// Goblin 몬스터의 구체 동작을 정의하는 클래스 (MonsterCore를 상속)
public class Monster_Goblin : MonsterCore
{
    private float timer; // 상태 지속 시간 측정용 타이머
    private float idleTime, patrolTime; // 각각 IDLE 및 PATROL 상태 지속 시간

    private float traceDist = 5f;    // 추적을 시작할 거리 기준
    private float attackDist = 1.5f; // 공격을 시작할 거리 기준

    private bool isAttack; // 공격 중인지 여부 (공격 중복 방지용)

    // 게임 시작 시 초기화 (체력, 속도, 공격 쿨타임 설정)
    void Start()
    {
        Init(10f, 3f, 2f); // 체력 10, 속도 3, 공격 딜레이 2초
    }

    // IDLE 상태: 제자리 대기하다가 랜덤 방향으로 순찰 시작
    public override void Idle()
    {
        timer += Time.deltaTime; // 대기 시간 측정

        // 대기 시간이 충분히 지나면 순찰 시작
        if (timer >= idleTime)
        {
            timer = 0f;

            // 랜덤 방향 지정: 1(오른쪽), -1(왼쪽)
            // 조건식 ? 참일 때 값 : 거짓일 때 값
            moveDir = Random.Range(0, 2) == 1 ? 1 : -1;

            // 방향에 맞춰 스프라이트 반전
            transform.localScale = new Vector3(moveDir, 1, 1);

            // 순찰 시간 무작위 설정
            patrolTime = Random.Range(1f, 5f);

            // 달리기 애니메이션 재생
            animator.SetBool("isRun", true);

            // 순찰 상태로 전환
            ChangeState(MonsterState.PATROL);
        }

        // 플레이어가 추적 범위에 있고, 바라보는 방향이면 추적 상태로 전환
        if (targetDist <= traceDist && isTrace)
        {
            timer = 0f;
            animator.SetBool("isRun", true);
            ChangeState(MonsterState.TRACE);
        }
    }

    // PATROL 상태: 일정 시간 동안 지정 방향으로 이동
    public override void Patrol()
    {
        // moveDir 방향으로 계속 이동
        transform.position += Vector3.right * moveDir * speed * Time.deltaTime;
        timer += Time.deltaTime;

        // 순찰 시간 종료 → IDLE로 복귀
        if (timer >= patrolTime)
        {
            timer = 0f;

            // 다음 대기 시간 무작위 설정
            idleTime = Random.Range(1f, 5f);

            // 달리기 애니메이션 해제
            animator.SetBool("isRun", false);

            // IDLE 상태로 전환
            ChangeState(MonsterState.IDLE);
        }

        // 플레이어가 감지 범위 안에 들어오면 추적 시작
        if (targetDist <= traceDist && isTrace)
        {
            timer = 0f;
            ChangeState(MonsterState.TRACE);
        }
    }

    // TRACE 상태: 플레이어 쪽으로 계속 이동
    public override void Trace()
    {
        // 플레이어를 향한 방향 벡터 계산
        var targetDir = (target.position - transform.position).normalized;

        // x축 방향으로만 이동 (y축 이동 없음)
        transform.position += Vector3.right * targetDir.x * speed * Time.deltaTime;

        // 이동 방향에 따라 스프라이트 좌우 반전
        var scaleX = targetDir.x > 0 ? 1 : -1;
        transform.localScale = new Vector3(scaleX, 1, 1);

        // 플레이어가 너무 멀어지면 추적 포기 → IDLE로 전환
        if (targetDist > traceDist)
        {
            animator.SetBool("isRun", false);
            ChangeState(MonsterState.IDLE);
        }

        // 일정 거리 이하로 가까워지면 공격 상태로 전환
        if (targetDist < attackDist)
        {
            ChangeState(MonsterState.ATTACK);
        }
    }

    // ATTACK 상태: 코루틴을 통해 일정 시간 공격 애니메이션 재생
    public override void Attack()
    {
        if (!isAttack)
            StartCoroutine(AttackRoutine()); // 한 번만 실행되도록 제한
    }

    // 실제 공격 로직 실행 코루틴
    IEnumerator AttackRoutine()
    {
        isAttack = true;

        // 공격 애니메이션 재생
        animator.SetTrigger("Attack");

        yield return new WaitForSeconds(1f); // 공격 모션 시간 (1초)

        // 이동 애니메이션 해제
        animator.SetBool("isRun", false);

        // 남은 공격 쿨타임 대기
        yield return new WaitForSeconds(attackTime - 1f);

        // 공격 종료
        isAttack = false;

        // 다시 IDLE 상태로 전환
        ChangeState(MonsterState.IDLE);
    }
}

 

 

기본 컴포넌트 자동 초기화 (Animator, Rigidbody, Collider)

animator = GetComponent<Animator>();
monsterRb = GetComponent<Rigidbody2D>();
monsterColl = GetComponent<Collider2D>();
  • 부모에서 한 번만 처리 → 자식 클래스는 컴포넌트 설정 고민 ❌
  • 추상화된 Init()에서 모두 통합 관리

 

Vector3 monsterDir = Vector3.right * moveDir;

여기서 몬스터 방향을 벡터화. 추후 내적에 활용하기 위함

 

자세한 내용은 주석 형태로 정리해 놓았다.

 

 

 

 

후기

*colldier 를 2D로 안넣는 실수를 했다. 주의할것

*애니메이터 Transition 조건과 시간 조절을 잘해서 자연스럽게 보이게 하자.

 

여러 코드 짜기 학습을 반복하다 보니 눈에 점점 익는 기분이 든다. 그래도 이걸 내 도구로 만들기 위해선 수업의 진도를 따라가는 것 외의 혼자 무언갈 구현해보는 시간이 필요할 것 같다. 고블린이 내가 코딩한대로 움직이는걸 보니 귀엽다(?)

 

 

촤아아악

반응형
salmu
@salmu :: SMU 각종 기록

목차