프로젝트 회고 / / 2025. 12. 2. 22:45

[DirectX 11] Enum의 한계 → FSM Component

반응형

🎮 구현 목표

복잡해지는 상태 관리를 체계적으로 개선하고, 유지보수성과 확장성을 높이기

WinAPI 프로젝트에서 Enum 기반 상태 관리의 한계를 직접 겪으며, DirectX 프로젝트에서는 FSM을 독립적인 Component로 분리하여 상태 전환 로직을 체계화하고자 했습니다.

 

🚨 문제 상황

 

WinAPI 프로젝트: Enum + Switch 문의 한계

 

핵심 문제점:

  1. 복합 상태 표현 불가:
    "공격+무적" 같은 상태는 bool 변수 추가 필요
  2. 조건문 과다:
    매 함수마다 if (IsHit) if (Invincibility) if (IsDead) 반복
  3. 상태 전환 분산:
    여러 곳에서 BodyState = LowerState::IDLE 직접 변경
  4. 디버깅 어려움:
    어디서 상태를 바꿨는지 추적 힘듦

상태를 추가하기도 어려울 뿐만 아니라 관리도 어려웠고, 애니메이션이 종료되는 시점 또는 입력이 끝나는 시점을 맞춰가며 동작 실행 중 다른 동작이 실행하거나 애니메이션이 재생되지 않도록 일일이 신경 써야 했습니다.

 

 

💭 해결 방안 고민

 

상태만 처리하는 별도의 클래스가 있다면 어떨까?

 

핵심 아이디어:

  • 상태가 바뀌면 애니메이션도 자동 변경
  • 각 상태별로 독립적인 함수에서 로직 관리
  • Tick에서 모든 상태 검사 → FSM이 현재 상태만 실행

이렇게 되면 Tick에서 모든 상태를 검사하던 방식에서 벗어나, 상태에 맞는 함수에서 변경 가능한 상태를 제한할 수 있게 되므로 훨씬 효율적일 것이라고 생각했습니다.

class UFSMStateManager
{
public:
    class FSMState
    {
    public:
        /** 최초 1회 실행 */
        std::function<void()> StartFunction = nullptr;

        /** 매 프레임 실행 */
        std::function<void(float)> UpdateFunction = nullptr;

        /** 상태 종료 시 실행 */
        std::function<void()> EndFunction = nullptr;
    };

    //...
private:
    FSMState* CurState = nullptr;
    FSMState* PrevState = nullptr;
    std::map<int, FSMState> States;
}

 

 

FSM을 아래와 같이 활용했습니다.

void AKnight::SetFSM()
{
    //            상태                Update          애니메이션
    CreateState(EKnightState::IDLE, &AKnight::SetIdle, "Idle");
    CreateState(EKnightState::RUN, &AKnight::SetRun, "Run");
}

void AKnight::CreateState(EKnightState _State, StateCallback _Callback, 
        std::string_view _AnimationName)
{
    FSM.CreateState(_State, std::bind(_Callback, this, std::placeholders::_1),
        [this, _AnimationName]()
        {
            std::string AnimationName = _AnimationName.data();
            BodyRenderer->ChangeAnimation(AnimationName);
        });
}
// 각 상태는 독립적인 함수
void AKnight::SetIdle(float _DeltaTime)
{
    ActivateGravity(); // 중력 체크
    RecoveryIdle(); // Idle 상태 초기화 함수

    // 입력에 따른 상태 전환
    if (UEngineInput::IsPress(VK_LEFT) || 
        UEngineInput::IsPress(VK_RIGHT))
    {
        FSM.ChangeState(EKnightState::IDLE_TO_RUN);
        return;
    }
   // ...
}

 

장점

  • Switch 문 없이 상태 등록 가능
  • 애니메이션 자동 연결
  • 각 상태가 독립적인 함수로 분리됨

 

 

🔧 한계

 

StartFunction 활용의 한계

StartFunction이 단일 함수만 받아서 애니메이션 재생에만 사용하게 되었습니다.
프로젝트 종료 후 복기하며 StartFunction을 vector로 만들었으면 "상태가 바뀌면 초기화 되어야 할 변수들도 StartFunction에서 처리했더라면 초기화 로직과 Tick 로직을 분리하고 더 간결했을 텐데"하는 생각이 들었습니다.

// 실제 구현 - StartFunction 1개만
void AKnight::SetFocus(float _DeltaTime)
{
    // ❌ 매 프레임 체크해야 함
    bIsFocusEffect = false;      // 초기화
    bIsFocusEndEffect = false;   // 초기화

    // 실제 로직
    if (UEngineInput::IsUp('A'))
    {
        ChangeNextState(EKnightState::IDLE);
    }
}

 

이렇게 했다면:

  • 애니메이션 재생
  • 플래그 초기화
  • 사운드 재생
  • 이펙트 스폰

 

 

// 이상적인 방식 (구현 못함)
CreateState(EKnightState::FOCUS)
    .AddStart([]() { 
        BodyRenderer->ChangeAnimation("Focus"); 
    })
    .AddStart([]() { 
        bIsFocusEffect = false;      // 최초 1회만
        bIsFocusEndEffect = false;   // 최초 1회만
    })
    .AddStart([]() { 
        Sound.Play("focus.wav"); 
    })
    .SetUpdate(&AKnight::SetFocus);  // 로직만 집중

 

 

 

✅ 결과

개선 효과:

항목 WinAPI (Before) DirectX (After)
상태 관리 Switch 문 직접 FSM Component
상태 추가 어려움 쉬움 (CreateState 1줄)
디버깅 전체 검색 필요 FSM.ChangeState 검색
가독성 ⭐⭐⭐
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유