TIL: AlivePlayer 시스템 심층 분석 (상태 머신, 데이터 관리, 컴포넌트 구조)

2025. 4. 28. 20:43[Unity] Game Programing

 

1. 개요

오늘 작업은 Unity 프로젝트 내 `AlivePlayer` 시스템의 전반적인 구조를 설계하고 구현하는 데 중점을 두었다. 플레이어의 다양한 행동(이동, 전투, 상호작용 등)을 체계적으로 관리하기 위해 계층적 상태 머신(Hierarchical State Machine) 패턴을 도입했으며, 플레이어 데이터 관리, 컴포넌트 통합, 애니메이션 처리 방식을 정의했다. 이 문서는 오늘 구현된 `AlivePlayer` 관련 코드들의 주요 내용과 그 과정에서 얻은 학습 내용을 상세히 기록한다.

주요 목표는 다음과 같다:

  • 플레이어의 상태(대기, 이동, 달리기, 상호작용, 공격, 피격 등)를 명확하게 분리하고 관리
  • 데이터(설정값, 애니메이션 파라미터, 상태 간 공유 데이터)를 효율적으로 구성하고 접근
  • 핵심 컴포넌트(`CharacterController`, `Animator`, `InteractionHandler`)와의 상호작용 방식을 정의
  • 확장 가능하고 유지보수가 용이한 코드 구조 설계

2. 핵심 `AlivePlayer` 클래스 (`AlivePlayer.cs`)

`AlivePlayer` 클래스는 플레이어 캐릭터를 나타내는 핵심 MonoBehaviour이다. 이 클래스는 플레이어의 기본적인 속성, 컴포넌트 참조, 상태 관리의 시작점 역할을 수행한다.

2.1. 주요 컴포넌트 및 의존성

  • `CharacterController`: Unity의 내장 컴포넌트로, 충돌 감지를 포함한 캐릭터의 물리적 이동을 처리한다. 캡슐 기반의 충돌체를 사용하며, 직접적인 물리 시뮬레이션보다는 스크립트에 의한 제어에 적합하다.
  • `Animator`: 플레이어 모델의 애니메이션을 제어한다. 하위 객체(Child GameObject)에 Animator 컴포넌트가 있을 것을 가정하고 `GetComponentInChildren`를 사용한다.
  • `InteractionHandler`: (코드는 제공되지 않았으나) 플레이어의 상호작용 관련 로직을 처리하는 사용자 정의 컴포넌트로 추정된다. `RequireComponent` 어트리뷰트를 통해 이 컴포넌트가 반드시 `AlivePlayer`와 같은 게임 오브젝트에 존재하도록 강제한다.
  • `CinemachineCamera`: (직접 참조 필드는 있으나 초기화 코드는 없음) Cinemachine 가상 카메라를 참조하여 카메라 제어 로직과 연동될 가능성을 시사한다.
  • `AnimatorOverrideController`: 런타임에 특정 애니메이션 클립을 교체할 수 있도록 한다. `Awake`에서 생성되어 기본 `runtimeAnimatorController`를 대체한다. 이는 상태별로 다른 상호작용 또는 공격 애니메이션을 동적으로 할당하는 데 사용될 수 있다.
// Awake에서 주요 컴포넌트 및 AnimatorOverrideController 초기화
public void Awake()
{
    InteractionHandler = GetComponent<InteractionHandler>();
    CharacterController = GetComponent<CharacterController>();
    Animator = GetComponentInChildren<Animator>();
    AnimationData = new AlivePlayerAnimationData(); // 애니메이션 파라미터 해시 초기화

    // Animator Override Controller 설정
    overrideController = new AnimatorOverrideController(Animator.runtimeAnimatorController);
    Animator.runtimeAnimatorController = overrideController;
}

2.2. 플레이어 스탯 (Stats)

플레이어의 다양한 상태(체력, 허기, 갈증 등)를 관리하기 위해 `ResourceStat`이라는 별도의 클래스(코드는 제공되지 않음)를 사용하는 것으로 보인다. 이는 각 스탯의 현재 값, 최대값, 증가/감소 로직 등을 캡슐화할 수 있는 좋은 방법이다.

  • `health`, `hungerPoint`, `waterPoint`, `stamina`, `temperature`, `sanity` 등 다양한 스탯이 정의되어 있다.
  • `Start` 메서드에서 일부 스탯(체력, 허기, 갈증, 스태미나, 온도)이 초기값 100으로 생성된다.
  • 외부에서 읽기 전용으로 접근할 수 있도록 public 프로퍼티 (`Health`, `HungerPoint` 등)를 제공한다.

2.3. 데미지 처리 및 이벤트

`IDamageable` 인터페이스를 구현하여 데미지를 받을 수 있는 객체임을 명시한다. TakeDamage 메서드는 데미지를 받아 체력을 감소시키고, 관련된 이벤트를 발생시킨다.

  • TakeDamage(float damage): 체력을 `damage`만큼 감소시키고 onDamaged 이벤트를 호출한다.
  • 체력이 0 이하가 되면 onDead 이벤트를 호출한다.
  • 이벤트(Action 타입)를 사용하여 데미지 발생(`onDamaged`) 및 사망(`onDead`) 시점을 다른 시스템(특히 상태 머신 내의 Combat State)에 알린다.
public event Action onDamaged;
public event Action onDead;

public void TakeDamage(float damage)
{
    Health.Subtract(damage); // ResourceStat 클래스의 메서드로 추정
    onDamaged?.Invoke(); // 이벤트 호출 (?. 연산자로 null 체크)

    if(Health.Current <= 0)
    {
        onDead?.Invoke(); // 이벤트 호출
    }
}

2.4. 상태 머신 연동

`AlivePlayer`는 직접 상태 로직을 가지는 대신, `AlivePlayerStateMachine`에게 상태 관리를 위임한다.

  • `Start` 메서드에서 `AlivePlayerStateMachine` 인스턴스를 생성한다.
  • `Update` 및 `FixedUpdate`에서 각각 상태 머신의 해당 업데이트 메서드를 호출하여 현재 활성 상태의 로직이 실행되도록 한다.

3. 계층적 상태 머신 구조

플레이어의 복잡한 행동 로직을 관리하기 위해 계층적 상태 머신 구조를 채택했다. 최상위 상태 머신(`AlivePlayerStateMachine`)이 존재하고, 이 머신은 다시 이동 관련 상태를 관리하는 `AlivePlayerMovementStateMachine`과 전투 관련 상태를 관리하는 `AlivePlayerCombatStateMachine`이라는 두 개의 하위 상태 머신을 포함한다. 이런 구조는 관련 상태들을 그룹화하여 복잡성을 줄이고 코드의 모듈성을 높인다.

3.1. 최상위 상태 머신 (`AlivePlayerStateMachine.cs`)

  • 역할: 전체 플레이어 상태 로직의 진입점. 이동과 전투라는 두 가지 주요 상태 그룹을 관리하는 하위 상태 머신들을 소유하고 초기화한다.
  • 주요 멤버:
    • `Player`: `AlivePlayer` 인스턴스 참조. 상태들이 플레이어의 컴포넌트나 데이터에 접근할 때 사용된다.
    • `ReusableData`: `AlivePlayerStateReusableData` 인스턴스. 여러 상태에서 공유되고 수정되어야 하는 데이터(예: 이동 입력값, 이동 속도 계수)를 담는다.
    • `MovementStateMachine`: 이동 관련 하위 상태 머신.
    • `CombatStateMachine`: 전투 관련 하위 상태 머신.
  • 초기화: 생성자에서 `Player` 참조를 받고, `ReusableData`와 두 하위 상태 머신(`MovementStateMachine`, `CombatStateMachine`)을 생성 및 초기화(`Init` 메서드 호출)한다.
  • 업데이트: `Update` 및 `FixedUpdate` 메서드는 단순히 하위 상태 머신들의 해당 업데이트 메서드를 호출하여 상태 로직 실행을 위임한다.
public class AlivePlayerStateMachine
{
    // ... 멤버 변수 ...

    public AlivePlayerStateMachine(AlivePlayer player)
    {
        Player = player;
        ReusableData = new AlivePlayerStateReusableData();

        // 하위 상태 머신 생성 및 초기화
        MovementStateMachine = new AlivePlayerMovementStateMachine(this);
        CombatStateMachine = new AlivePlayerCombatStateMachine(this);

        MovementStateMachine.Init(); // 각 하위 머신의 초기 상태 설정
        CombatStateMachine.Init();
    }

    public void Update()
    {
        MovementStateMachine.Update(); // 이동 상태 로직 업데이트
        CombatStateMachine.Update(); // 전투 상태 로직 업데이트
    }

    public void FixedUpdate()
    {
        MovementStateMachine.FixedUpdate(); // 이동 상태 물리 업데이트
        CombatStateMachine.FixedUpdate(); // 전투 상태 물리 업데이트
    }
}

3.2. 이동 상태 머신 (`AlivePlayerMovementStateMachine.cs`)

  • 역할: 플레이어의 이동과 관련된 상태(서있기, 걷기/달리기, 스프린트, 상호작용 중 이동 제한 등)를 관리한다. `StateMachine`이라는 기본 클래스(코드는 제공되지 않음)를 상속받아 상태 변경 로직(`ChangeState`) 등을 활용하는 것으로 추정된다.
  • 주요 상태:
    • `IdlingState`: 기본 대기 상태.
    • `RunningState`: 일반적인 이동(걷기 또는 달리기) 상태.
    • `SprintingState`: 전력 질주 상태.
    • `InterctingState`: 상호작용 상태 (오타 수정 필요: `InteractingState`).
  • 초기화 (`Init`): 각 상태 클래스의 인스턴스를 생성하고, 초기 상태를 `IdlingState`로 설정한다. 각 상태는 생성될 때 최상위 상태 머신(`AlivePlayerStateMachine`)의 참조를 받아 `Player`나 `ReusableData` 등에 접근할 수 있다.

3.3. 전투 상태 머신 (`AlivePlayerCombatStateMachine.cs`)

  • 역할: 플레이어의 전투와 관련된 상태(기본 전투 자세, 조준, 공격, 피격, 사망 등)를 관리한다. 역시 `StateMachine`을 상속받는다.
  • 주요 상태:
    • `CombatIdlingState`: 기본적인 비전투/전투 대기 상태.
    • `AttackingState`: 공격 중인 상태.
    • `AimingState`: 조준 중인 상태.
    • `DamagedState`: 피격 당한 상태.
    • `DeadState`: 사망 상태.
  • 초기화 (`Init`): 각 전투 상태 클래스의 인스턴스를 생성하고, 초기 상태를 `CombatIdlingState`로 설정한다.

핵심 학습: 이동과 전투 로직을 별도의 하위 상태 머신으로 분리함으로써, 각 상태 머신은 자신이 책임지는 영역에만 집중할 수 있다. 이는 코드의 복잡도를 낮추고, 특정 기능(예: 새로운 이동 스킬 추가, 다른 무기 공격 방식 구현)을 확장하기 용이하게 만든다. 최상위 상태 머신은 이들을 통합하고 조율하는 역할을 한다.

4. 상태 클래스 구현 (`AlivePlayerState`, `MovementState`, `CombatState`)

모든 구체적인 상태 로직은 `AlivePlayerState`를 직간접적으로 상속받는 클래스들에 구현된다.

4.1. 기본 상태 클래스 (`AlivePlayerState.cs`)

  • 역할: 모든 플레이어 상태 클래스의 추상 기반 클래스. `IState` 인터페이스(코드는 제공되지 않음)를 구현하며, 모든 상태가 공통적으로 가져야 할 메서드(Enter, Exit, Update, FixedUpdate)와 유틸리티 메서드를 제공한다.
  • 주요 메서드:
    • `Enter()`: 상태에 진입할 때 호출된다. 기본 구현은 디버그 로그를 출력하고 입력 액션 콜백을 추가한다 (`AddInputActionCallbacks`).
    • `Exit()`: 상태에서 벗어날 때 호출된다. 기본 구현은 입력 액션 콜백을 제거한다 (`RemoveInputActionCallbacks`).
    • `Update()`, `FixedUpdate()`: 매 프레임, 물리 프레임마다 호출된다. 기본 구현은 비어 있으며, 하위 클래스에서 구체적인 로직을 구현한다.
    • `StartAnimation(int hash)`, `StopAnimation(int hash)`: 애니메이터의 bool 파라미터를 제어하여 애니메이션을 시작하거나 중지하는 헬퍼 메서드. `AnimationData`에서 미리 계산된 해시값을 사용한다.
    • `AddInputActionCallbacks()`, `RemoveInputActionCallbacks()`: 상태 진입/종료 시 특정 입력 액션에 대한 이벤트 리스너를 등록/해제하기 위한 가상 메서드. 각 상태는 필요한 입력에 대해서만 리스너를 관리할 수 있다.
    • `GetNormalizedTime(Animator animator, string tag, int layerIndex)`: 특정 태그를 가진 애니메이션 상태의 정규화된 재생 시간(0~1)을 반환한다. 애니메이션 재생 완료 여부 등을 확인하는 데 사용될 수 있다.

4.2. 이동 상태 베이스 (`AlivePlayerMovementState.cs`)

  • 역할: 모든 이동 관련 상태(`IdlingState`, `RunningState` 등)의 기반 클래스. `AlivePlayerState`를 상속받으며, 이동에 공통적으로 필요한 로직을 구현한다.
  • 주요 로직:
    • 입력 읽기 (`ReadMovementInput`): `Managers.Input` (싱글톤 패턴 추정)을 통해 이동(Move) 및 스프린트(Sprint) 입력을 읽어 `ReusableData`에 저장한다.
    • 이동 처리 (`Move`): `ReusableData`의 입력값과 속도 계수를 사용하여 `CharacterController.Move`를 호출하여 실제 이동을 수행한다. 수직 속도(중력 등)와 수평 이동을 분리하여 처리한다.
    • 회전 처리 (`RotationPlayer`): 이동 방향을 기반으로 목표 회전 각도를 계산하고, `Quaternion.Slerp`를 사용하여 부드러운 회전을 구현한다. 회전 속도는 `AlivePlayerSO`에서 가져온다.
    • 상호작용 애니메이션 설정 (`SetInteractAnimation`): `AnimatorOverrideController`를 사용하여 "Interaction"이라는 이름의 애니메이션 클립을 동적으로 설정하고 재생 속도를 조절하는 메서드를 제공한다. 이는 `InteractingState`에서 사용될 수 있다.
    • 입력 콜백 관리: 이동 입력(Move)이 취소되었을 때(`canceled`) 호출될 `OnMovementCanceled` 메서드를 등록/해제한다. 구체적인 동작은 하위 상태에서 정의할 수 있다.
// AlivePlayerMovementState의 Update 메서드
public override void Update()
{
    ReadMovementInput(); // InputManager에서 이동/스프린트 입력 읽기
    Move(); // 읽은 입력 기반으로 이동 및 회전 처리
}

private void Move()
{
    // 수직 속도 적용 (중력 등)
    stateMachine.Player.CharacterController.Move(stateMachine.ReusableData.VerticalVelocity * Time.deltaTime);

    // 수평 이동 입력 및 속도 계수 확인
    if(stateMachine.ReusableData.MovementInput == Vector2.zero || stateMachine.ReusableData.MovementSpeedModifier == 0f)
    {
        return; // 이동 입력 없으면 중단
    }

    Vector3 movementDirection = GetMovementInputDirection(); // 입력 기반 이동 방향 벡터 계산

    RotationPlayer(movementDirection); // 해당 방향으로 플레이어 회전

    // 실제 수평 이동 적용 (CharacterController 사용)
    stateMachine.Player.CharacterController.Move(movementDirection * stateMachine.ReusableData.MovementSpeedModifier * Time.deltaTime);
}

4.3. 전투 상태 베이스 (`AlivePlayerCombatState.cs`)

  • 역할: 모든 전투 관련 상태(`CombatIdlingState`, `AttackingState` 등)의 기반 클래스. `AlivePlayerState`를 상속받으며, 전투 상황에서 공통적으로 필요한 로직, 특히 데미지 및 사망 이벤트 처리를 구현한다.
  • 주요 로직:
    • 이벤트 구독/해지: `Enter`에서 `AlivePlayer`의 `onDamaged`, `onDead` 이벤트에 대한 리스너(`OnDamaged`, `OnDead`)를 등록하고, `Exit`에서 해지한다. 이를 통해 전투 상태에 있을 때만 피격/사망 처리를 수행하도록 보장한다.
    • 피격 처리 (`OnDamaged`): `onDamaged` 이벤트가 발생하면 피격 애니메이션을 재생 (`StartAnimation`)하고, `CheckAnimation` 코루틴(UniTask 사용)을 시작하여 애니메이션이 특정 지점(0.8)까지 재생되면 애니메이션 파라미터를 다시 false로 설정한다.
    • 사망 처리 (`OnDead`): `onDead` 이벤트가 발생하면 사망 애니메이션을 재생한다.
    • 공격 애니메이션 설정 (`SetAttackAnimation`): `AnimatorOverrideController`를 사용하여 "Attack" 애니메이션 클립을 동적으로 설정하고 재생 속도를 조절한다. 이는 `AttackingState`에서 사용될 것이다.
    • UniTask 활용 (`CheckAnimation`): 피격 애니메이션 재생 완료를 기다리기 위해 `async UniTaskVoid`와 `await UniTask.Yield()`를 사용한다. 이는 Update 루프를 차단하지 않고 비동기적으로 애니메이션 상태를 폴링하는 효율적인 방법이다.
// AlivePlayerCombatState의 이벤트 처리 부분
public override void Enter()
{
    base.Enter();
    // 전투 상태 진입 시 데미지/사망 이벤트 구독
    stateMachine.Player.onDamaged += OnDamaged;
    stateMachine.Player.onDead += OnDead;
}

public override void Exit()
{
    base.Exit();
    // 전투 상태 종료 시 이벤트 구독 해지
    stateMachine.Player.onDamaged -= OnDamaged;
    stateMachine.Player.onDead -= OnDead;
}

private void OnDamaged()
{
    StartAnimation(stateMachine.Player.AnimationData.DamagedParameterHash); // 피격 애니메이션 시작
    CheckAnimation().Forget(); // 애니메이션 완료 체크 비동기 실행 (UniTask)
}

async UniTaskVoid CheckAnimation() // UniTask를 사용한 비동기 메서드
{
    while (true)
    {
        // "Damage" 태그를 가진 애니메이션의 정규화된 시간 가져오기 (Layer 2)
        float normalizedTime = GetNormalizedTime(stateMachine.Player.Animator, "Damage", 2);

        if(normalizedTime >= 0.8f) // 애니메이션이 80% 이상 재생되면
        {
            StopAnimation(stateMachine.Player.AnimationData.DamagedParameterHash); // 애니메이션 중지
            break; // 루프 탈출
        }
        await UniTask.Yield(); // 다음 프레임까지 대기
    }
}

5. 데이터 관리 전략

플레이어 관련 데이터를 효율적으로 관리하기 위해 여러 종류의 데이터 클래스 및 ScriptableObject를 사용한다.

5.1. 플레이어 설정 데이터 (`AlivePlayerSO.cs`)

  • 역할: ScriptableObject를 사용하여 플레이어의 기본 설정값(이동 속도, 스프린트 속도, 회전 속도 등)을 에디터에서 쉽게 조정하고 관리할 수 있도록 한다.
  • 장점:
    • 코드 변경 없이 값을 수정하고 여러 프리팹에서 공유하기 용이하다.
    • 프로젝트 내에서 에셋으로 존재하므로 관리가 편리하다.
    • `[field: SerializeField]`와 `[field: Range]` 어트리뷰트를 사용하여 인스펙터에서 보기 좋고 안전하게 편집할 수 있도록 설정되어 있다.
  • 현재 `MoveSpeed`, `SprintSpeed`, `RotationSpeed`가 정의되어 있다.

5.2. 애니메이션 파라미터 데이터 (`AlivePlayerAnimationData.cs`)

  • 역할: Animator에서 사용되는 파라미터 이름(문자열)을 미리 정수 해시값으로 변환하여 저장한다. 런타임에 `Animator.SetBool/SetFloat` 등을 호출할 때 문자열 대신 정수 해시를 사용하면 성능상 이점이 있다.
  • 구현:
    • `[Serializable]` 어트리뷰트로 클래스가 직렬화 가능함을 명시한다 (하지만 현재 코드에서는 직접 인스펙터에서 편집하기보다는 `AlivePlayer`의 `Awake`에서 생성됨).
    • 생성자에서 `Animator.StringToHash()`를 사용하여 모든 파라미터 이름 문자열을 정수 해시로 변환하고 `public int` 프로퍼티에 저장한다.
    • 이동 상태 (Idling, Running 등), 전투 상태 (Aiming, Attacking 등), 그룹핑 파라미터 (Ground, Moving 등)에 대한 해시값을 관리한다.
// AlivePlayerAnimationData 생성자
public AlivePlayerAnimationData()
{
    // 문자열 파라미터 이름을 정수 해시로 변환하여 저장
    GroundParameterHash = Animator.StringToHash(groundParameterHashName);
    MovingParameterHash = Animator.StringToHash(movingParameterHashName);
    // ... 기타 모든 파라미터들 ...
    DeadParameterHash = Animator.StringToHash(deadParameterHashName);
}

5.3. 상태 간 공유 데이터 (`AlivePlayerStateReusableData.cs`)

  • 역할: 상태 머신 내의 여러 상태들이 공유하고 수정해야 하는 데이터를 담는 컨테이너 클래스. 예를 들어, 이동 상태에서 읽은 입력값은 다른 상태(예: 점프 상태)에서도 사용될 수 있다.
  • 주요 데이터:
    • `MovementInput`: 현재 프레임의 이동 입력 벡터 (Vector2).
    • `MovementSpeedModifier`: 현재 상태에 따른 이동 속도 계수 (기본값 1f). 예를 들어, 조준 중에는 이 값을 낮출 수 있다.
    • `VerticalVelocity`: 중력 등 수직 방향 속도 (Vector3). `CharacterController`는 자체적인 중력 처리가 없으므로 직접 관리해야 한다.
    • `ShouldSprint`: 스프린트 입력이 눌렸는지 여부 (bool).
  • `AlivePlayerStateMachine`이 이 클래스의 인스턴스를 소유하며, 각 상태는 `stateMachine.ReusableData`를 통해 이 데이터에 접근한다.

핵심 학습: 데이터를 목적에 따라 분리(설정값: SO, 애니메이션 파라미터: Data Class, 상태 공유 데이터: Data Class)하면 코드의 각 부분이 명확한 책임을 갖게 된다. 특히 SO를 활용한 설정 관리와 애니메이션 파라미터 해싱은 각각 유연성과 성능 최적화에 기여한다.

6. 종합적인 학습 내용 및 고찰

  • 계층적 상태 머신의 효용성: 복잡한 행동 로직을 이동/전투 등 관련 그룹으로 묶어 관리하는 것이 단일 상태 머신보다 훨씬 구조적이고 확장성이 좋다는 것을 확인했다.
  • 상태 기반 클래스 설계: 공통 로직(애니메이션 제어, 입력 콜백 관리, 이벤트 처리)을 기반 클래스(`AlivePlayerState`, `MovementState`, `CombatState`)에 구현하고, 구체적인 상태 로직은 하위 클래스에서 구현하는 방식이 코드 중복을 줄이고 각 상태의 책임을 명확히 한다.
  • 데이터 중심 설계: 설정값(`AlivePlayerSO`), 애니메이션 파라미터(`AnimationData`), 상태 공유 데이터(`ReusableData`)를 명확히 분리하여 관리하는 것이 중요하다. 이는 데이터 접근 및 수정을 용이하게 하고 코드의 의존성을 낮춘다.
  • Unity 컴포넌트 활용: `CharacterController`를 이용한 이동 구현, `Animator`와 `AnimatorOverrideController`를 이용한 유연한 애니메이션 처리 방식에 대해 이해를 높였다.
  • 이벤트 기반 통신: `Action` 이벤트를 사용하여 `AlivePlayer`의 상태 변화(피격, 사망)를 `CombatState`에 알리는 방식은 클래스 간의 직접적인 결합도를 낮추는 좋은 방법이다.
  • 비동기 처리 (UniTask): 애니메이션 재생 완료 확인과 같은 작업에 `UniTask`를 사용하면 Update 루프를 방해하지 않고 깔끔하게 비동기 로직을 구현할 수 있다.
  • 코드 가독성 및 네이밍: 클래스, 메서드, 변수의 이름을 명확하게 짓고, 로직을 적절한 메서드로 분리하는 것이 코드 이해와 유지보수에 필수적이다. (`InterctingState` 같은 오타는 수정 필요)
  • Input System 연동: `Managers.Input`이라는 추상화된 레이어를 통해 Unity의 Input System 액션(`InputAction`)에 접근하고, `performed`, `canceled` 등의 콜백을 활용하는 방식을 사용했다.

개선 및 추가 고려 사항

  • 각 구체적인 상태 클래스 (`IdlingState`, `RunningState`, `AttackingState` 등)의 상세 로직 구현이 필요하다. 현재는 상태 머신 구조와 기반 클래스 위주로 구현되어 있다.
  • 오류 처리 및 예외 상황(예: 필요한 컴포넌트 누락)에 대한 방어 코드가 추가되면 안정성이 향상될 것이다.
  • 테스트 코드 작성을 통해 각 상태의 전이 조건과 행동이 올바르게 작동하는지 검증하는 것이 좋다.