C++ 행동 패턴 — Command·Observer·State
행동 패턴 개요
섹션 제목: “행동 패턴 개요”행동 패턴(Behavioral Pattern)은 객체 간의 상호작용과 책임 분배 방식을 정의합니다. 구조 패턴이 클래스 구성에 집중한다면, 행동 패턴은 런타임 동작 흐름을 어떻게 조율할지에 초점을 맞춥니다.
게임 개발에서 특히 중요한 세 가지 행동 패턴을 다룹니다.
- Command: 요청을 객체로 캡슐화해 Undo/Redo, 리플레이 시스템 구현
- Observer: 이벤트 기반 알림으로 UI 업데이트, 게임 이벤트 시스템 구현
- State: 캐릭터 상태 머신을 명확하게 구조화
Command 패턴
섹션 제목: “Command 패턴”Command 패턴은 “무엇을 실행할 것인가”를 객체로 감쌉니다. 이를 통해 실행 요청을 큐에 쌓거나, 되돌리거나(Undo), 나중에 재실행(Redo)할 수 있습니다.
C++ 구현
섹션 제목: “C++ 구현”#include <memory>#include <stack>#include <vector>#include <iostream>
// Command 인터페이스class ICommand{public: virtual ~ICommand() = default; virtual void execute() = 0; virtual void undo() = 0;};
// 수신자(Receiver): 실제 작업을 수행하는 객체class Character{ int x = 0, y = 0;public: void move(int dx, int dy) { x += dx; y += dy; std::cout << "Position: (" << x << ", " << y << ")\n"; } // undo를 위해 역방향 이동 제공 void moveBack(int dx, int dy) { move(-dx, -dy); }};
// 구체 커맨드: 이동class MoveCommand : public ICommand{ Character& character; int dx, dy;public: MoveCommand(Character& ch, int dx, int dy) : character(ch), dx(dx), dy(dy) {}
void execute() override { character.move(dx, dy); } void undo() override { character.moveBack(dx, dy); }};
// 인보커(Invoker): 커맨드를 관리하고 실행class CommandManager{ std::stack<std::unique_ptr<ICommand>> history;public: void executeCommand(std::unique_ptr<ICommand> cmd) { cmd->execute(); history.push(std::move(cmd)); }
void undoLast() { if (history.empty()) return; history.top()->undo(); history.pop(); }};
// 사용 예시int main(){ Character player; CommandManager manager;
manager.executeCommand(std::make_unique<MoveCommand>(player, 1, 0)); // (1,0) manager.executeCommand(std::make_unique<MoveCommand>(player, 0, 1)); // (1,1) manager.undoLast(); // (1,0)으로 복귀}게임 개발 활용
섹션 제목: “게임 개발 활용”- Undo/Redo 시스템: 에디터나 턴제 게임에서 행동 취소
- 리플레이 시스템: 커맨드 큐를 저장했다가 재실행하면 동일한 게임 플레이 재현
- 입력 리매핑: 키 입력을 커맨드 객체로 매핑하면 런타임에 조작 변경 가능
- AI 행동 계획: AI가 계획한 행동을 커맨드 배열로 표현하고 순차 실행
Observer 패턴
섹션 제목: “Observer 패턴”Observer 패턴은 Subject(주제) 객체의 상태가 변경될 때 등록된 모든 Observer(관찰자)에게 자동으로 알림을 보냅니다. 발행-구독(Pub-Sub) 모델의 기초입니다.
C++ 구현 (템플릿 기반)
섹션 제목: “C++ 구현 (템플릿 기반)”#include <functional>#include <unordered_map>#include <vector>#include <algorithm>
// 이벤트 타입을 enum으로 정의enum class GameEvent{ PlayerDied, LevelUp, ItemPickup, EnemyDefeated};
// 이벤트 데이터 (다형성 활용 가능)struct EventData{ int intValue = 0; float floatValue = 0.f; // 필요에 따라 확장};
// EventDispatcher: Subject 역할class EventDispatcher{ using Handler = std::function<void(const EventData&)>; std::unordered_map<GameEvent, std::vector<Handler>> listeners;
public: // 리스너 등록 (람다, 멤버함수 포인터 모두 가능) void subscribe(GameEvent event, Handler handler) { listeners[event].push_back(std::move(handler)); }
// 이벤트 발행 void dispatch(GameEvent event, const EventData& data = {}) const { auto it = listeners.find(event); if (it == listeners.end()) return; for (const auto& handler : it->second) { handler(data); } }};
// 전역 또는 싱글턴으로 사용EventDispatcher gEventDispatcher;
// Observer 사용 예시class HUD{public: HUD() { // 람다로 이벤트 구독 gEventDispatcher.subscribe(GameEvent::LevelUp, [this](const EventData& data) { onLevelUp(data.intValue); });
gEventDispatcher.subscribe(GameEvent::PlayerDied, [this](const EventData&) { showGameOver(); }); }
private: void onLevelUp(int newLevel) { // UI 업데이트 로직 } void showGameOver() { /* ... */ }};
// Subject(발행자) 사용 예시class Player{ int level = 1;public: void gainExperience(int exp) { // 레벨업 조건 충족 시 이벤트 발행 EventData data; data.intValue = ++level; gEventDispatcher.dispatch(GameEvent::LevelUp, data); }};주의사항: 댕글링 포인터
섹션 제목: “주의사항: 댕글링 포인터”Observer가 먼저 소멸하면 등록된 핸들러가 해제된 메모리를 참조할 수 있습니다. 해결책으로는 다음이 있습니다.
- 구독 취소(unsubscribe) 메서드로 소멸자에서 해제
std::weak_ptr로 Observer 참조를 약하게 유지- 핸들러 ID를 반환해 명시적 구독 해제
State 패턴
섹션 제목: “State 패턴”State 패턴은 객체의 내부 상태에 따라 행동이 달라질 때, 각 상태를 별도 클래스로 분리합니다. 복잡한 if-else 또는 switch 분기를 제거하고 상태 전환 로직을 명확하게 합니다.
C++ 구현
섹션 제목: “C++ 구현”#include <memory>#include <iostream>
class Character; // 전방 선언
// State 인터페이스class ICharacterState{public: virtual ~ICharacterState() = default; virtual void update(Character& character, float deltaTime) = 0; virtual void onEnter(Character& character) {} virtual void onExit(Character& character) {}};
// 컨텍스트: 상태를 보유하는 캐릭터class Character{ std::unique_ptr<ICharacterState> currentState; float speed = 0.f;
public: float moveInput = 0.f; bool isGrounded = true; bool jumpPressed = false;
explicit Character(std::unique_ptr<ICharacterState> initialState) { transitionTo(std::move(initialState)); }
void transitionTo(std::unique_ptr<ICharacterState> newState) { if (currentState) currentState->onExit(*this); currentState = std::move(newState); currentState->onEnter(*this); }
void update(float deltaTime) { currentState->update(*this, deltaTime); }
void setSpeed(float s) { speed = s; } float getSpeed() const { return speed; }};
// 구체 상태: 유휴(Idle)class IdleState : public ICharacterState{public: void onEnter(Character& ch) override { ch.setSpeed(0.f); std::cout << "[State] Idle\n"; }
void update(Character& ch, float dt) override; // (아래에서 정의)};
// 구체 상태: 이동(Run)class RunState : public ICharacterState{public: void onEnter(Character& ch) override { ch.setSpeed(300.f); std::cout << "[State] Run\n"; }
void update(Character& ch, float dt) override;};
// 구체 상태: 점프(Jump)class JumpState : public ICharacterState{ float timeInAir = 0.f;public: void onEnter(Character& ch) override { timeInAir = 0.f; std::cout << "[State] Jump\n"; }
void update(Character& ch, float dt) override { timeInAir += dt; if (timeInAir > 0.8f && ch.isGrounded) { // 착지 후 입력에 따라 상태 전환 if (ch.moveInput != 0.f) ch.transitionTo(std::make_unique<RunState>()); else ch.transitionTo(std::make_unique<IdleState>()); } }};
// update 정의 (순환 의존 해결 후)void IdleState::update(Character& ch, float dt){ if (ch.jumpPressed) ch.transitionTo(std::make_unique<JumpState>()); else if (ch.moveInput != 0.f) ch.transitionTo(std::make_unique<RunState>());}
void RunState::update(Character& ch, float dt){ if (ch.jumpPressed) ch.transitionTo(std::make_unique<JumpState>()); else if (ch.moveInput == 0.f) ch.transitionTo(std::make_unique<IdleState>());}계층적 상태 머신(HSM)
섹션 제목: “계층적 상태 머신(HSM)”기본 State 패턴을 확장해 상태를 계층으로 구성하면 “공중에 있는 동안 공격 가능” 같은 복합 상태를 간결하게 표현할 수 있습니다. Unreal Engine의 Animation Blueprint State Machine과 Unity의 Animator Controller가 이 개념을 구현한 예입니다.
세 패턴의 비교
섹션 제목: “세 패턴의 비교”| 패턴 | 핵심 목적 | 게임 적용 사례 |
|---|---|---|
| Command | 요청 캡슐화 | Undo/Redo, 리플레이, 입력 처리 |
| Observer | 느슨한 이벤트 알림 | UI 갱신, 도전 과제, 퀘스트 추적 |
| State | 상태별 행동 분리 | 캐릭터 FSM, AI 상태, 게임 모드 전환 |
마무리
섹션 제목: “마무리”Command, Observer, State 패턴은 독립적으로도 강력하지만 함께 사용하면 시너지가 극대화됩니다. 예를 들어 State 패턴으로 AI 상태를 관리하고, 상태 전환 시 Observer로 이벤트를 발행하며, 플레이어의 모든 행동을 Command로 기록하면 완성도 높은 게임 아키텍처를 구성할 수 있습니다.
다음 단계로는 Strategy 패턴(알고리즘 교체), Chain of Responsibility 패턴(입력 처리 체인), Visitor 패턴(다형적 연산 분리)을 학습하는 것을 권장합니다.