반응형
🎮 구현 목표
복잡해지는 상태 관리를 체계적으로 개선하고, 유지보수성과 확장성을 높이기
WinAPI 프로젝트에서 Enum 기반 상태 관리의 한계를 직접 겪으며, DirectX 프로젝트에서는 FSM을 독립적인 Component로 분리하여 상태 전환 로직을 체계화하고자 했습니다.
🚨 문제 상황
WinAPI 프로젝트: Enum + Switch 문의 한계
핵심 문제점:
- 복합 상태 표현 불가:
"공격+무적" 같은 상태는 bool 변수 추가 필요 - 조건문 과다:
매 함수마다if (IsHit) if (Invincibility) if (IsDead)반복 - 상태 전환 분산:
여러 곳에서BodyState = LowerState::IDLE직접 변경 - 디버깅 어려움:
어디서 상태를 바꿨는지 추적 힘듦
상태를 추가하기도 어려울 뿐만 아니라 관리도 어려웠고, 애니메이션이 종료되는 시점 또는 입력이 끝나는 시점을 맞춰가며 동작 실행 중 다른 동작이 실행하거나 애니메이션이 재생되지 않도록 일일이 신경 써야 했습니다.
💭 해결 방안 고민
상태만 처리하는 별도의 클래스가 있다면 어떨까?
핵심 아이디어:
- 상태가 바뀌면 애니메이션도 자동 변경
- 각 상태별로 독립적인 함수에서 로직 관리
- 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 검색 |
| 가독성 | ⭐ | ⭐⭐⭐ |
반응형
'프로젝트 회고' 카테고리의 다른 글
| [UE5 액션] 루트 모션 기반 자연스러운 대시 구현 Motion Warping (0) | 2025.12.02 |
|---|---|
| [UE5 액션] 유연한 콤보 시스템: 이벤트 기반 Perfect/Mercy 구간을 통한 공격 후딜레이 캔슬 및 콤보 연계 (0) | 2025.12.02 |
| [DirectX 11] 다중 좌표계 간 위치 동기화 (0) | 2025.12.02 |
| [UE5 액션] 애니메이션 라이프 사이클 기반 비동기 상태 관리 (0) | 2025.12.02 |
| [UE5 액션] 상태 관리: StateComponent와 GameplayTag (0) | 2025.12.02 |
