DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 8. State Machine & Delegate

Vfly 2025. 9. 3. 00:35

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

GitHub - HyangRim/DirectX11-Engine-Client

Contribute to HyangRim/DirectX11-Engine-Client development by creating an account on GitHub.

github.com


 

State Machine 패턴과 Delegate 시스템을 활용한 캐릭터 시스템

1. State Machine 패턴 이론

1.1 State패턴이란?

State 패턴은 객체의 내부 상태가 변경될 때 해당 객체의 행동을 바꿀 수 있는 행위 패턴이다. 

  • 상태 캡슐화 : 각 상태를 별도의 클래스로 분리
  • 행동 위임 : 현재 상태에 따라 적절한 행동을 위임
  • 상태 전환 제어 : 명확한 규칙에 따른 상태 변경
// 전통적인 방식 (문제가 있는 코드)
void Character::Update() {
    if (state == IDLE) {
        // 대기 로직
        if (input == MOVE) state = RUNNING;
        if (input == ATTACK) state = ATTACKING;
    } else if (state == RUNNING) {
        // 이동 로직
        if (input == STOP) state = IDLE;
        if (input == JUMP) state = JUMPING;
    } else if (state == ATTACKING) {
        // 공격 로직...
    }
    // 상태가 많아질수록 복잡해짐
}

 

 

이런 방식은 상태가 많아질수록 복잡성이 기하급수적으로 증가하며, 새로운 상태 추가나 로직 변경이 어렵다.

 

 

2. Delegate 패턴 이론

2.1 Delegate 패턴이란?

Delegate 패턴은 상속 대신 객체 합성을 통해 코드 재사용을 달성하는 디자인 패턴이다. 한 객체가 다른 객체에게 작업을 위임(delegate)하여 기능을 확장한다.

  • 송신자(Sender) : 작업을 위임하는 객체
  • 수신자(Receiver) : 위임받은 작업을 수행하는 객체
  • 느슨한 결합 : 인터페이스를 통한 연결로 유연성 확보

 

3. 시스템 아키텍쳐 : PlayerStateMachine과 AnimationStateMachine 분리

이번 프로젝트에서 구현한 시스템의 가장 큰 특징은 로직 상태와 애니메이션 상태를 분리한 것이다. 

 

전체 구조도

Character GameObject
├── PlayerStateMachine (로직 제어)
│   ├── WaitState
│   ├── RunState  
│   ├── SkillStates (Q, W, E, R)
│   └── CraftState
├── AnimationStateMachine (애니메이션 제어)
│   ├── WaitState
│   ├── RunState
│   ├── SkillStates
│   └── CraftState
└── Delegate System (상태 간 통신)

 

3.1 State 패턴 : 3계층 상태 관리 시스템

// 플레이어 행동 상태
enum class PlayerStateType {
    Wait, Run, Skill_1, Skill_2, Skill_3, Skill_4, 
    Craft, Die, BaseAttack, Counter
};

// 애니메이션 상태  
enum class AnimationStateType {
    Wait, Run, BaseAttack, Skill_1, Skill_2, Skill_3, Skill_4,
    Charging, Releasing, Cooldown, Death, Craft, Counter
};

// 몬스터 AI 상태
enum class MonsterStateType {
    Wait, Appear, Run, Death, Dying, Attack, Trace
};

 

3개의 독립적인 상태머신을 설계하여 관심사를 분리

  • PlayerStateMachine : 게임 로직과 입력 처리
  • AnimationStateMachine : 애니메이션 재생 관리
  • MonsterStateMachine : AI 행동 패턴

 

3.2 상태 전환의 유연성 확보

class PlayerState {
public:
    virtual void Enter() = 0;
    virtual void Update() = 0;
    virtual void Exit() = 0;
    virtual bool CanTransitionTo(PlayerStateType newState) = 0;
    
    // 상태별 특성 정의
    virtual bool IsCharging() const { return false; }
    virtual bool IsReleasing() const { return false; }
    virtual bool IsMovable() const { return true; }
};

각 상태 마다 전환 조건을 세밀하게 제어할 수 있도록 설계

 

 

3.1 PlayerStateMachine 구현

class PlayerStateMachine : public Component
{
public:
    // 상태 요청 및 관리
    void RequestStateChange(PlayerStateType newState);
    bool CanChangeState(PlayerStateType newState);
    
    // 상태 조회
    PlayerStateType GetCurrentState() const;
    shared_ptr<PlayerState> GetCurrentStatePtr() const;
    bool IsInState(PlayerStateType state) const;

    // Delegate 시스템
    Delegate::Delegate<bool&> OnTryCraft;        // 제작 시도
    Delegate::Delegate<bool&> OnCraftCompleted;  // 제작 완료
    Delegate::Delegate<bool&> OnQSkillCompleted; // Q스킬 완료
    // ... 기타 스킬 완료 델리게이트들

private:
    void ExecuteStateChange(PlayerStateType newState);
    void HandleStateChangeRequest(shared_ptr<EventData> eventData);
    
    // 입력 처리
    void HandleSkillInput();
    void HandleCraftInput();
    void CheckCraftCompletion();

private:
    unordered_map<PlayerStateType, shared_ptr<PlayerState>> m_states;
    shared_ptr<PlayerState> m_currentState;
    shared_ptr<AnimationStateMachine> m_animationStateMachine;
    queue<PlayerStateType> m_stateChangeQueue;
};

 

3.2 AnimationStateMachine 구현

class AnimationStateMachine : public Component
{
public:
    void RequestStateChange(AnimationStateType newState);
    void RegisterState(AnimationStateType type, shared_ptr<AnimationState> state);
    
    // 애니메이션 상태 확인
    bool IsCurrentAnimationCompleted() const;
    float GetCurrentAnimationProgress() const;

private:
    void ExecuteStateChange(AnimationStateType newState);
    void CheckAnimationCompletion();
    void HandleAutoTransitions(); // 자동 전환 처리

private:
    unordered_map<AnimationStateType, shared_ptr<AnimationState>> m_states;
    shared_ptr<AnimationState> m_currentState;
    shared_ptr<ModelAnimator> m_animator;
};

 

3.3 MonsterStateMachine 구현


class MonsterStateMachine : public Component
{
public:
    MonsterStateMachine();
    virtual ~MonsterStateMachine();

    // Component 메서드 오버라이드
    virtual void Start() override;
    virtual void Update() override;
    virtual void OnDestroy() override;

    // 상태 관리
    void RequestStateChange(MonsterStateType newState);
    bool CanChangeState(MonsterStateType newState) const;

private:
    // 상태 전환 실제 실행
    void ExecuteStateChange(MonsterStateType newState);

    // AI 로직
    void ProcessAI();
    void UpdateTargetDetection();

private:
    // 상태 관리
    unordered_map<MonsterStateType, shared_ptr<MonsterState>> m_states;
    shared_ptr<MonsterState> m_currentState;
 
    // 상태 전환 대기열
    queue<MonsterStateType> m_stateChangeQueue;
};

 

 

4. 실제 상태 구현 예시

4.1 제작(Craft) 상태 구현

PlayerState 구현

class NickyCraftState : public PlayerState
{
public:
    void Enter() override {
        cout << "니키 제작 상태 진입" << endl;
        m_craftTime = 0.f;
        m_isStarted = true;
        
        // 애니메이션 상태 변경 요청
        if (m_animationStateMachine) {
            m_animationStateMachine->RequestStateChange(AnimationStateType::Craft);
        }
    }

    void Update() override {
        if (!m_isStarted) return;
        
        m_craftTime += DT;
        
        // Delegate를 통한 완료 확인
        bool isCompleted = false;
        m_stateMachine->OnCraftCompleted.Invoke(isCompleted);
        
        if (isCompleted) {
            cout << "제작 완료!" << endl;
            m_stateMachine->RequestStateChange(PlayerStateType::Wait);
        }
    }

private:
    float m_craftTime = 0.f;
    bool m_isStarted = false;
};

 

AnimationState 구현

class NickyAnimCraftState : public AnimationState
{
public:
    void Enter(shared_ptr<ModelAnimator> animator) override {
        if (!animator) return;
        
        animator->SetAnimationByTag(L"Craft", false);
        animator->SetNextAnimationSpeed(m_playSpeed);
        
        m_skillTime = 0.0f;
        m_isSkillComplete = false;
        
        cout << "Nicky Craft 애니메이션 시작" << endl;
    }

    void Update(shared_ptr<ModelAnimator> animator) override {
        m_skillTime += DT;
        
        // 시간 기반 완료 체크
        if (!m_isSkillComplete && m_skillTime >= m_expectedDuration) {
            m_isSkillComplete = true;
            cout << "Craft 애니메이션 완료!" << endl;
        }
    }

    bool CanTransitionTo(AnimationStateType nextState) override {
        return m_isSkillComplete && nextState == AnimationStateType::Wait;
    }

private:
    float m_skillTime = 0.0f;
    bool m_isSkillComplete = false;
    float m_playSpeed = 1.f;
};

 

4.2 스킬 상태 구현 예시

Q스킬 상태

class NickyQState : public PlayerState {
private:
    enum class QSkillChargeState {
        ChargingFromWait,     // 정지 상태에서 차징
        ChargingFromRun,      // 달리기 상태에서 차징  
        ChargingWaitLoop,     // 정지 차징 루프
        ChargingRunLoop,      // 달리기 차징 루프
        Releasing, Complete
    };
    
    bool m_isCharging;
    bool m_isReleasing;
    float m_chargeTime;
};

 

State 내부에 하위 State를 두어 복잡한 스킬 로직을 체계적으로 관리

 

5. Delegate 시스템의 활용

5.1 템플릿 기반 범용 Delegate 구현

template<typename ..._Args>
class Delegate {
    using Function = std::function<void(_Args...)>;
    std::vector<Function> m_functionList;

public:
    inline void operator+=( Function&& _func) {
        m_functionList.push_back(_func);
    }
    
    inline void operator()(_Args... _types) {
        for (auto& func : m_functionList) {
            func(_types...);
        }
    }
};

 

 

5.2 Delegate를 통한 상태 간 통신

void LumiaIsland::Start() {
    // 제작 완료 확인 Delegate 등록
    m_player->GetPlayerStateMachine()->OnCraftCompleted.Push([this](bool& completed) {
        completed = IsCraftStateCompleted();
    });

    // Q스킬 완료 확인 Delegate 등록
    m_player->GetPlayerStateMachine()->OnQSkillCompleted.Push([this](bool& completed) {
        completed = IsQSkillCompleted();
    });
    
    // 기타 스킬 완료 확인들...
}

// 실제 완료 확인 로직
bool LumiaIsland::IsCraftStateCompleted() {
    auto currentState = m_player->GetAnimationStateMachine()->GetCurrentState();
    if (currentState == AnimationStateType::Craft) {
        auto craftState = dynamic_pointer_cast<NickyAnimCraftState>(
            m_player->GetAnimationStateMachine()->GetCurrentStatePtr());
        if (craftState) {
            return craftState->IsCompleted();
        }
    }
    return false;
}

(사실 이 부분은 따로 클래스를 만들어 분리하는게 맞는거같은데.... 시간 상 일단 냅둠)

 

5.3 함수 바인딩 시스템

// 멤버 함수 바인딩
template<typename R, typename T, typename... Args>
constexpr auto Bind(R(T::* f)(Args...), T* p) {
    return [p, f](Args... args) -> R { 
        return (p->*f)(args...); 
    };
}

// 상속 관계 고려한 바인딩 (SFINAE 활용)
template<typename R, typename T, typename B, typename... Args, 
         Base_Check<T, B> = NULL>
constexpr auto Bind(R(T::* f)(Args...), B* p) {
    return [p, f](Args... args) -> R { 
        return (static_cast<T*>(p)->*f)(args...); 
    };
}

 

6. 상태 전환 시스템

6.1 상태 변경 큐 시스템

void PlayerStateMachine::Update() {
    // 상태 변경 요청 처리
    while (!m_stateChangeQueue.empty()) {
        PlayerStateType nextState = m_stateChangeQueue.front();
        m_stateChangeQueue.pop();
        
        if (CanChangeState(nextState)) {
            ExecuteStateChange(nextState);
            break; // 한 프레임에 하나씩만 처리
        }
    }
    
    // 현재 상태 업데이트
    if (m_currentState) {
        m_currentState->Update();
    }
    
    // 입력 처리
    if (m_inputEnabled) {
        ProcessInput();
    }
}

 

상태변경은 EventManager를 통해 호출됨

 

 

7. 시스템의 장점

7.1 관심사의 분리

  • 로직 상태 : 게임 규칙, 데미지, 쿨타임 등
  • 애니메이션 상태 : 애니메이션 재생, 프레임 타이밍 등

7.2 확장성

새로운 캐릭터나 스킬 추가 시 기존 코드 수정 없이 새로운 State 클래스만 추가하면 된다.

// 새 스킬 추가 예시
class NickyNewSkillState : public PlayerState {
    // 새 스킬 로직 구현
};

// 시스템에 등록
stateMachine->RegisterState(PlayerStateType::NewSkill, 
                           make_shared<NickyNewSkillState>());

 

 

8. 성능 고려 사항

8.1 상태 변경 최적화

bool PlayerStateMachine::CanChangeState(PlayerStateType newState) {
    if (m_currentState->GetType() == newState) {
        return false; // 같은 상태로 변경 방지
    }
    
    // 현재 상태에서 전환 가능한지 확인
    return m_currentState->CanTransitionTo(newState);
}

8.2 메모리 관리

void PlayerStateMachine::InitializeStates() {
    // 모든 상태 미리 생성
    m_states[PlayerStateType::Wait] = make_shared<NickyWaitState>();
    m_states[PlayerStateType::Run] = make_shared<NickyRunState>();
    m_states[PlayerStateType::Skill_1] = make_shared<NickyQState>();
    // ...
}

 

 

 

결론

State Machine 패턴과 Delegate 시스템을 결합한 캐릭터 시스템은 복잡한 게임 로직을 체계적으로 관리 할 수 잇게 해준다.

특히 PSM과 ASM의 분리는 로직과 표현의 관심사를 명확히 분리하여 코드의 가독성과 유지보수성을 향상시켰다.

 

  1. 확장성 있는 설계
    1. 새로운 캐릭터 추가 시 기존 코드 수정 없이 새로운 State 클래스들만 구현
    2. Delegate를 통한 느슨한 결합으로 컴포넌트 독립성 확보
  2. 복잡한 상태 관리
    1. 3계층 상태머신으로 관심사 분리
    2. 상태 전환 조건의 세밀한 제어
    3. 큐 기반 상태 전환으로 동시 요청 처리
  3. 스킬 시스템의 유연성
    1. 각 캐릭터별 고유 스킬 로직을 독립적으로 관리
    2. 차징, 즉시발동, 지속형 등 다양한 스킬 타입 개발 가능
  4. 애니메이션 동기화
    1. 게임 로직과 애니메이션의 완벽한 동기화 
    2. 상태 기반으로 애니메이션 전환 제어
  5. AI 시스템
void MonsterStateMachine::ProcessAI() {
    // 상태별 AI 로직 분리
    switch (currentState) {
    case MonsterStateType::Trace:
        if (distanceToTarget <= m_attackRange) {
            RequestStateChange(MonsterStateType::Attack);
        }
        break;
    case MonsterStateType::Attack:
        if (distanceToTarget > m_attackRange) {
            RequestStateChange(MonsterStateType::Trace);
        }
        break;
    }
}