본문 바로가기
유니티

Day 40 - 상태머신(3D 방치형 게임 만들기 中)

by shin0707 2024. 6. 18.
728x90

  • 주제

>> 상태 인터페이스 (IState)

>> 상태머신 추상 클래스 (StateMachine)


>> 플레이어 상태머신 클래스 (PlayerStateMachine)
>> 이동 상태 클래스 (PlayerMovingState)

>> 공격 상태 클래스 (PlayerAttackingState)

>> 추적 상태 클래스 (PlayerChasingState)


  • 공부내용

1. 상태 인터페이스(IState)

상태 인터페이스는 상태들이 구현해야 하는 메서드들을 간략하게 정의한다.(공통 메서드 정의)

Enter는 상태에 진입할 때, Exit는 상태에서 벗어날 때 호출된다.

// 상태 인터페이스
public interface IState
{
    void Enter(); // 상태 진입
    void Exit();  // 상태 종료
}

2. 상태머신 추상 클래스 (StateMachine)

상태머신 추상 클래스는 현재 상태를 추적하고 상태를 변경하는 기능을 제공한다.

ChangeState 메서드는 현재 상태를 종료하고(Exit), 새로운 상태를 시작한다.(Enter).

 

특정 조건이 만족되면, ChangeState 메서드를 호출하여 상태를 전환할 수 있다.

예를 들어, PlayerMovingState에서 몬스터를 감지하면, PlayerChasingState로 전환된다.

// 상태머신 추상클래스
public abstract class StateMachine
{
    protected IState currentState; // 현재 상태

    public void ChangeState(IState state)
    {
        currentState?.Exit(); // 현재 상태 종료
        currentState = state; // 새로운 상태 설정
        currentState?.Enter(); // 새로운 상태 진입
    }
}

3. 플레이어 상태머신 클래스 (PlayerStateMachine)

플레이어 상태머신 클래스는 플레이어의 상태와 애니메이터, 이동 속도, 공격 간격 등을 관리한다.

각 상태 객체 (MovingState, AttackingState, ChasingState)를 초기화한다.

// 플레이어 상태머신 클래스
public class PlayerStateMachine : StateMachine
{
    public Player Player { get; } // 플레이어
    public Animator Animator { get; } // 애니메이터

    // 상태들
    public PlayerMovingState MovingState { get; } // 이동 상태
    public PlayerAttackingState AttackingState { get; } // 공격 상태
    public PlayerChasingState ChasingState { get; } // 추적 상태

    public float MoveSpeed { get; } // 이동 속도
    public float AttackInterval { get; } // 공격 간격

    // 생성자
    public PlayerStateMachine(Player player, Animator animator, float moveSpeed, float attackInterval)
    {
        this.Player = player; // 플레이어 설정
        this.Animator = animator; // 애니메이터 설정

        MoveSpeed = moveSpeed; // 이동 속도 설정
        AttackInterval = attackInterval; // 공격 간격 설정

        MovingState = new PlayerMovingState(this); // 이동 상태 초기화
        AttackingState = new PlayerAttackingState(this, animator); // 공격 상태 초기화
        ChasingState = new PlayerChasingState(this); // 추적 상태 초기화
    }
}

 


4. 이동 상태 클래스 (PlayerMovingState)

 

  • Enter: 이동 코루틴을 시작(개별 메서드 작성)
  • Exit: 이동 코루틴을 종료(개별 메서드 작성)

StopCoroutine 메서드 사용법

1. StopCoroutine(string methodName) – 메서드 이름을 문자열로 제공

2. StopCoroutine(IEnumerator routine) – 코루틴의 인스턴스를 제공

 

기존의 방식: Exit 메서드에서 StopCoroutine(Move())를 호출 

-> 버그 발생(이동이 멈추지 않음)

-> 원인: StopCoroutine(Move())를 호출할 때, 새로운 IEnumerator 인스턴스가 생성됨. 이 인스턴스는 StartCoroutine으로 시작된 실제 코루틴 인스턴스와 다르기 때문에 StopCoroutine이 해당 코루틴을 멈출 수 없음.

 

현재 방식: 특정 코루틴의 실행을 추적해서, 필요 시 정확히 해당 코루틴을 중지(코루틴 인스턴스 추적)

1. 코루틴 인스턴스 저장

StartCoroutine을 호출할 때 반환되는 Coroutine 객체를 변수에 저장(현재 실행 중인 코루틴 추적)

ex) moveCoroutine = stateMachine.Player.StartCoroutine(Move());

 

2. 코루틴 종료

StopCoroutine을 호출할 때, 저장된 Coroutine 객체를 사용하여 정확히 해당 코루틴을 중지

ex) stateMachine.Player.StopCoroutine(moveCoroutine);

 

3. 코루틴 변수 초기화

코루틴이 중지된 후, 해당 변수 (moveCoroutine)를 null로 설정하여 더 이상 코루틴이 실행 중이지 않음을 나타냄


 

 

 

  • Move: 매 프레임마다 플레이어를 앞으로 이동시키고, 몬스터를 감지하면 추적 상태로 전환
  • DetectMonster: 몬스터를 감지하여 추적 상태로 전환할지 여부를 결정

 

// 이동 상태 클래스
public class PlayerMovingState : IState
{
    private PlayerStateMachine stateMachine; // 상태머신
    private Coroutine moveCoroutine; // 이동 코루틴

    // 생성자
    public PlayerMovingState(PlayerStateMachine stateMachine)
    {
        this.stateMachine = stateMachine; // 상태머신 설정
    }

    public void Enter()
    {
        StartMoveCoroutine(); // 이동 코루틴 시작
    }

    public void Exit()
    {
        StopMoveCoroutine(); // 이동 코루틴 종료
    }

    // 이동 코루틴
    private IEnumerator Move()
    {
        while (true)
        {
            stateMachine.Player.transform.Translate(Vector3.forward * stateMachine.MoveSpeed * Time.deltaTime); // 플레이어 이동

            if (DetectMonster()) // 몬스터 감지
            {
                stateMachine.ChangeState(stateMachine.ChasingState); // 추적 상태로 전환
                yield break;
            }

            yield return null;
        }
    }

    // 몬스터 감지
    private bool DetectMonster()
    {
        Collider[] hitColliders = Physics.OverlapSphere(stateMachine.Player.transform.position, stateMachine.Player.detectionRadius, LayerMask.GetMask("Monster"));
        foreach (var hitCollider in hitColliders)
        {
            Vector3 directionToMonster = hitCollider.transform.position - stateMachine.Player.transform.position;
            float angle = Vector3.Angle(stateMachine.Player.transform.forward, directionToMonster);
            if (angle < stateMachine.Player.detectionAngle / 2)
            {
                return true; // 몬스터 발견
            }
        }
        return false; // 몬스터 미발견
    }

    // 이동 코루틴 시작
    public void StartMoveCoroutine()
    {
        if (moveCoroutine == null)
        {
            moveCoroutine = stateMachine.Player.StartCoroutine(Move());
        }
    }

    // 이동 코루틴 종료
    public void StopMoveCoroutine()
    {
        if (moveCoroutine != null)
        {
            stateMachine.Player.StopCoroutine(moveCoroutine);
            moveCoroutine = null;
        }
    }
}

5. 공격 상태 클래스 (PlayerAttackingState)

 

  • Enter: 공격 코루틴을 시작
  • Exit: 공격 코루틴을 종료
  • Attack: 공격 애니메이션을 트리거하고 일정 시간 동안 대기

 

// 공격 상태 클래스
public class PlayerAttackingState : IState
{
    private PlayerStateMachine stateMachine; // 상태머신
    private Animator animator; // 애니메이터

    // 생성자
    public PlayerAttackingState(PlayerStateMachine stateMachine, Animator animator)
    {
        this.stateMachine = stateMachine; // 상태머신 설정
        this.animator = animator; // 애니메이터 설정
    }

    public void Enter()
    {
        stateMachine.Player.StartCoroutine(Attack()); // 공격 코루틴 시작
    }

    public void Exit()
    {
        stateMachine.Player.StopCoroutine(Attack()); // 공격 코루틴 종료
    }

    // 공격 코루틴
    private IEnumerator Attack()
    {
        animator.SetTrigger("Slash"); // Slash 애니메이션 트리거 설정
        yield return new WaitForSeconds(stateMachine.AttackInterval); // 공격 간격 대기
    }
}

6. 추적 상태 클래스 (PlayerChasingState)

 

  • Enter: 추적 코루틴을 시작
  • Exit: 추적 코루틴을 종료
  • Chase: 매 프레임마다 플레이어를 몬스터 방향으로 이동시키고, 가장 가까운 몬스터 수색
  • FindTargetMonster: 탐지 반경 내에서 가장 가까운 몬스터를 수색
// 추적 상태 클래스
public class PlayerChasingState : IState
{
    private PlayerStateMachine stateMachine; // 상태머신
    private Coroutine chaseCoroutine; // 추적 코루틴
    private Transform targetMonster; // 목표 몬스터

    // 생성자
    public PlayerChasingState(PlayerStateMachine stateMachine)
    {
        this.stateMachine = stateMachine; // 상태머신 설정
    }

    public void Enter()
    {
        StartChaseCoroutine(); // 추적 코루틴 시작
    }

    public void Exit()
    {
        StopChaseCoroutine(); // 추적 코루틴 종료
    }

    // 추적 코루틴
    private IEnumerator Chase()
    {
        while (true)
        {
            FindTargetMonster(); // 목표 몬스터 찾기
            if (targetMonster != null)
            {
                Vector3 direction = (targetMonster.position - stateMachine.Player.transform.position).normalized;
                stateMachine.Player.transform.Translate(direction * stateMachine.MoveSpeed * Time.deltaTime); // 몬스터를 향해 이동
            }
            yield return null;
        }
    }

    // 목표 몬스터 찾기
    private void FindTargetMonster()
    {
        Collider[] hitColliders = Physics.OverlapSphere(stateMachine.Player.transform.position, stateMachine.Player.detectionRadius, LayerMask.GetMask("Monster"));
        float closestDistance = float.MaxValue;
        Transform closestMonster = null;

        foreach (var hitCollider in hitColliders)
        {
            Vector3 directionToMonster = hitCollider.transform.position - stateMachine.Player.transform.position;
            float angle = Vector3.Angle(stateMachine.Player.transform.forward, directionToMonster);
            if (angle < stateMachine.Player.detectionAngle / 2)
            {
                float distance = directionToMonster.magnitude;
                if (distance < closestDistance)
                {
                    closestDistance = distance;
                    closestMonster = hitCollider.transform;
                }
            }
        }

        targetMonster = closestMonster; // 가장 가까운 몬스터 설정
    }

    // 추적 코루틴 시작
    public void StartChaseCoroutine()
    {
        if (chaseCoroutine == null)
        {
            chaseCoroutine = stateMachine.Player.StartCoroutine(Chase());
        }
    }

    // 추적 코루틴 종료
    public void StopChaseCoroutine()
    {
        if (chaseCoroutine != null)
        {
            stateMachine.Player.StopCoroutine(chaseCoroutine);
            chaseCoroutine = null;
        }
    }
}

 

728x90
반응형