본문 바로가기
게임 개발

[Unity] 보스 키우기 게임 (2) - FSM

by chobbo 2024. 6. 20.

 

이번 프로젝트에서는 유한 상태 머신( FSM )을 사용했다.

 

Player와 Enemy 둘 다 움직이거나 공격하는 부분에서

공통적인 부분이 많으므로 Character를 상속받아 구현하였다.

 

구조

CharacterStateMachine을 상속받는 PlayerStateMachine, EnemyStateMachine이 존재하고

각 머신들은 플레이어의 State, Enemy의 State를 생성자로 가진다.

StateMachine 구조

 

자식 머신들은 공통적으로 본인의 데이터(CharacterSO)와 스크립트(Character),

그리고 공격 대상의 Health 스크립트를 가지므로 CharacterStateMachine에서 선언해주었다.

 

public class EnemyStateMachine : CharacterStateMachine
{
    public EnemyIdleState IdleState {  get; private set; }
    public EnemyMoveState MoveState {  get; private set; }
    public EnemyAttackState AttackState { get; private set; }

    public EnemyStateMachine(Enemy enemy)
    {
        character = enemy;
        Data = (EnemySO)character.Data;
        Target = CharacterManager.Instance.Player.GetComponent<Health>();

        IdleState = new EnemyIdleState(this);
        MoveState = new EnemyMoveState(this);
        AttackState = new EnemyAttackState(this);
    }
}

 

그리고 이런식으로 자식 머신의 생성자에서 변수에 맞는 값을 세팅해주었다.

 


 

그리고 모든 캐릭터들의 상태의 공통된 부분을 관리하는 CharacterBaseState,

그리고 이를 상속받는 CharacterIdleState, CharacterMoveState, CharacterAttackState를 구현하였다.

State 구조

 

Player와 Enemy는 각각의 상태를 상속받아 각자가 가지고 있는 상태를 업데이트한다.

 

 

! 여기서 문제가 생겼다 

 protected CharacterStateMachine stateMachine;

 

CharacterBaseState는 다음과 같이 CharacterStateMachine 형식의 stateMachine 변수를 가지고 있다.

근데 우리가 EnemyMoveState.cs와 같은 스크립트에서 사용하고자 하는 stateMachine은

EnemyStateMachine 형식이다.

(ex) ChangeState(stateMachine.IdleState);

 

 

즉 Player의 상태 스크립트에서는 PlayerStateMachine 형식의 stateMachine,

Enemy의 상태 스크립트에서는 EnemyStateMachine 형식의 stateMachine이 필요한 것.

 

Animator이나 Controller는 적이나 플레이어 각각에 따로 존재하지만,

Component의 형식이 같으므로 문제가 되지 않았는데 

상태 머신은 형식이 다르므로 문제가 되었다.

 

 

결국 다음과 같이 생성될 때 형변환을 해주었다.

    private EnemyStateMachine stateMachine;
    public EnemyIdleState(CharacterStateMachine stateMachine) : base(stateMachine)
    {
        this.stateMachine = stateMachine as EnemyStateMachine;
    }

 

더 좋은 방법이 있는지는 아직 모르겠다.

설계한 FSM 구조 상 EnemyBaseState.cs 스크립트를 또 만들 수는 없을 것 같다.

 

더보기

형 변환에는 많은 방법이 있다.

      -  this.stateMachine = (EnemyStateMachine)stateMachine;
      -  this.stateMachine = stateMachine as EnemyStateMachine; -> 이게 더 좋은 방법 같아서 사용했다

 

 


사실 처음에는 Enemy와 Player의 공통된 속성을 Character.cs로 빼지 않고

각자의 스크립트에서 작성하였다.

 

다음과 같았다.

 

그런데 이렇게 따로 작성하고 보니

또 문제가 발생했다..... 

 

원래 CharacterBaseState.cs 스크립트에서 

    protected void StartAnimation(int animatorHash)
    {
        stateMachine.character.Animator.SetBool(animatorHash, true);
    }

    protected void StopAnimation(int animatorHash)
    {
        stateMachine.character.Animator.SetBool(animatorHash, false);
    }

 

다음과 같이 애니메이션을 세팅해주는 함수를 선언하여

아래 상태 스크립트들이 이 함수를 사용할 수 있어야 했는데,

 

Player와 Enemy 각각의 정보를 CharacterStateMachine에서 들고 있을 수 없기 때문에

위의 함수에서 Animator를 변수를 사용할 수 없는 이슈가 발생했다.

 

이유

 CharacterStateMachine는 Player와 Enemy의 공통된 기능을 담당하는 부모 클래스이므로,
 이 클래스가 특정 자식 클래스(Player나 Enemy)의 정보를 직접 가지고 있으면 
 코드의 재사용성과 유연성이 떨어진다.
 
 부모 클래스는 자식 클래스의 세부사항에 대해 알지 않고, 공통된 기능과 인터페이스만을 제공해야 한다!

 

 

이를 해결하기 위해

플레이어와 Enemy의 부모 클래스인 Character.cs를 만들어 

CharacterStateMachine에서 Character.cs에 대한 변수를 들고 있도록 했다.

 

 

이렇게!

 

 


최최최종 FSM 완성

 

끄읏