게임 개발에서 자주 쓰는 디자인 패턴
GoF 디자인 패턴은 게임 개발에서도 핵심 문제들을 해결하는 데 자주 등장합니다. 그러나 게임의 맥락에서는 교과서적 구현보다 성능과 사용 편의성을 함께 고려한 실용적 변형이 더 자주 쓰입니다. 여기서는 게임에서 가장 활용도가 높은 네 가지 패턴을 실무 관점으로 정리합니다.
1. Command 패턴 — Undo/Redo 시스템
섹션 제목: “1. Command 패턴 — Undo/Redo 시스템”입력 처리나 게임 액션을 객체로 캡슐화합니다. 명령 히스토리를 유지하면 Undo/Redo가 자연스럽게 구현됩니다.
// ICommand 인터페이스class ICommand{public: virtual ~ICommand() = default; virtual void Execute() = 0; virtual void Undo() = 0;};
// 구체적인 명령class MoveCommand : public ICommand{ GameObject& _target; glm::vec3 _delta; glm::vec3 _prevPos;
public: MoveCommand(GameObject& target, glm::vec3 delta) : _target(target), _delta(delta) {}
void Execute() override { _prevPos = _target.Position; _target.Position += _delta; }
void Undo() override { _target.Position = _prevPos; }};
// 커맨드 관리자class CommandHistory{ std::stack<std::unique_ptr<ICommand>> _history; std::stack<std::unique_ptr<ICommand>> _redoStack;
public: void Execute(std::unique_ptr<ICommand> cmd) { cmd->Execute(); _history.push(std::move(cmd)); // 새 명령 실행 시 redo 스택 초기화 while (!_redoStack.empty()) _redoStack.pop(); }
void Undo() { if (_history.empty()) return; _history.top()->Undo(); _redoStack.push(std::move(_history.top())); _history.pop(); }
void Redo() { if (_redoStack.empty()) return; _redoStack.top()->Execute(); _history.push(std::move(_redoStack.top())); _redoStack.pop(); }};2. State 패턴 — 캐릭터 FSM
섹션 제목: “2. State 패턴 — 캐릭터 FSM”캐릭터 상태 전환 로직을 상태 클래스로 분리합니다. switch/if-else 중첩을 제거하고 상태별 진입/탈출 로직을 명확히 합니다.
class Character;
class ICharacterState{public: virtual ~ICharacterState() = default; virtual void Enter(Character& c) {} virtual void Update(Character& c, float dt) = 0; virtual void Exit(Character& c) {}};
class IdleState : public ICharacterState{public: void Update(Character& c, float dt) override { if (c.IsMoving()) c.SetState(std::make_unique<MoveState>()); if (c.IsAttackInput()) c.SetState(std::make_unique<AttackState>()); }};
class AttackState : public ICharacterState{ float _timer = 0.f; static constexpr float AttackDuration = 0.5f;
public: void Enter(Character& c) override { c.PlayAnimation("Attack"); }
void Update(Character& c, float dt) override { _timer += dt; if (_timer >= AttackDuration) c.SetState(std::make_unique<IdleState>()); }
void Exit(Character& c) override { c.StopAnimation("Attack"); }};
class Character{ std::unique_ptr<ICharacterState> _state;
public: Character() : _state(std::make_unique<IdleState>()) {}
void SetState(std::unique_ptr<ICharacterState> newState) { if (_state) _state->Exit(*this); _state = std::move(newState); _state->Enter(*this); }
void Update(float dt) { if (_state) _state->Update(*this, dt); }
bool IsMoving() const { return /* 이동 입력 감지 */ false; } bool IsAttackInput() const { return /* 공격 입력 감지 */ false; } void PlayAnimation(const char*) {} void StopAnimation(const char*) {}};3. Observer 패턴 — 이벤트 버스
섹션 제목: “3. Observer 패턴 — 이벤트 버스”게임 오브젝트 간 직접 참조 없이 이벤트를 전달합니다. Unity의 UnityEvent, UE5의 Delegate/Multicast가 이 패턴의 엔진 구현체입니다.
#include <functional>#include <unordered_map>#include <vector>#include <typeindex>#include <any>
class EventBus{ using Handler = std::function<void(const std::any&)>; std::unordered_map<std::type_index, std::vector<Handler>> _handlers;
public: template<typename EventT> void Subscribe(std::function<void(const EventT&)> handler) { _handlers[typeid(EventT)].push_back( [handler](const std::any& e) { handler(std::any_cast<const EventT&>(e)); }); }
template<typename EventT> void Publish(const EventT& event) { auto it = _handlers.find(typeid(EventT)); if (it == _handlers.end()) return; for (auto& h : it->second) h(event); }};
// 이벤트 타입struct PlayerDiedEvent { int playerId; };struct EnemyKilledEvent { int enemyId; int score; };
// 사용EventBus bus;
bus.Subscribe<EnemyKilledEvent>([](const EnemyKilledEvent& e){ std::cout << "Enemy " << e.enemyId << " killed! +" << e.score << " pts\n";});
bus.Publish(EnemyKilledEvent{42, 100});4. Flyweight 패턴 — 메모리 최적화
섹션 제목: “4. Flyweight 패턴 — 메모리 최적화”수천 개의 동일한 데이터(메시, 텍스처, 파티클 설정)를 공유해 메모리를 절약합니다.
// 불변 공유 데이터 (Flyweight)struct BulletData{ std::string meshPath; float speed; float damage; float radius;};
// 가변 인스턴스 데이터struct BulletInstance{ const BulletData* data; // 공유 참조 glm::vec3 position; glm::vec3 velocity; float lifetime;};
class BulletFactory{ std::unordered_map<std::string, BulletData> _pool;
public: const BulletData* GetData(const std::string& type) { auto [it, inserted] = _pool.try_emplace(type); if (inserted) { // 최초 1회 로드 it->second = LoadBulletData(type); } return &it->second; }
private: BulletData LoadBulletData(const std::string& type) { // 파일/설정에서 로드 return { type + ".mesh", 50.f, 10.f, 0.1f }; }};
// 1000개 총알: BulletData는 1개, BulletInstance는 1000개BulletFactory factory;std::vector<BulletInstance> bullets(1000);for (auto& b : bullets) b.data = factory.GetData("pistol_bullet");5. 패턴 선택 가이드
섹션 제목: “5. 패턴 선택 가이드”| 문제 | 패턴 | 이유 |
|---|---|---|
| Undo/Redo, 리플레이 | Command | 명령을 객체로 기록 |
| 캐릭터/AI 상태 전환 | State | 상태별 로직 분리 |
| 오브젝트 간 결합 제거 | Observer | 직접 참조 없이 이벤트 전달 |
| 대량 오브젝트 메모리 | Flyweight | 공유 데이터 풀링 |
| 오브젝트 생성 추상화 | Factory/Pool | 생성 비용 및 타입 결합 제거 |
- Command 패턴은 입력 처리를 객체화해 Undo/Redo와 리플레이 시스템을 자연스럽게 구현한다.
- State 패턴은 캐릭터 FSM에서 if/switch 중첩을 제거하고, Enter/Exit 훅으로 상태 전환 부수 효과를 캡슐화한다.
- Observer/이벤트 버스는 오브젝트 간 직접 참조를 없애 결합도를 낮추지만, 구독 해제를 잊으면 메모리 누수가 발생한다.
- Flyweight는 수천 개의 동일한 오브젝트에서 공유 가능한 불변 데이터를 분리해 메모리를 절약한다.