콘텐츠로 이동

게임 개발에서 자주 쓰는 디자인 패턴

GoF 디자인 패턴은 게임 개발에서도 핵심 문제들을 해결하는 데 자주 등장합니다. 그러나 게임의 맥락에서는 교과서적 구현보다 성능과 사용 편의성을 함께 고려한 실용적 변형이 더 자주 쓰입니다. 여기서는 게임에서 가장 활용도가 높은 네 가지 패턴을 실무 관점으로 정리합니다.


입력 처리나 게임 액션을 객체로 캡슐화합니다. 명령 히스토리를 유지하면 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();
}
};

캐릭터 상태 전환 로직을 상태 클래스로 분리합니다. 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*) {}
};

게임 오브젝트 간 직접 참조 없이 이벤트를 전달합니다. 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");

문제패턴이유
Undo/Redo, 리플레이Command명령을 객체로 기록
캐릭터/AI 상태 전환State상태별 로직 분리
오브젝트 간 결합 제거Observer직접 참조 없이 이벤트 전달
대량 오브젝트 메모리Flyweight공유 데이터 풀링
오브젝트 생성 추상화Factory/Pool생성 비용 및 타입 결합 제거

  • Command 패턴은 입력 처리를 객체화해 Undo/Redo와 리플레이 시스템을 자연스럽게 구현한다.
  • State 패턴은 캐릭터 FSM에서 if/switch 중첩을 제거하고, Enter/Exit 훅으로 상태 전환 부수 효과를 캡슐화한다.
  • Observer/이벤트 버스는 오브젝트 간 직접 참조를 없애 결합도를 낮추지만, 구독 해제를 잊으면 메모리 누수가 발생한다.
  • Flyweight는 수천 개의 동일한 오브젝트에서 공유 가능한 불변 데이터를 분리해 메모리를 절약한다.