ゲームを作り始めたころ、こんな経験はありませんか?
- 敵キャラを増やすたびにコピペが増えて収拾がつかなくなった
- シーン遷移に
if文やswitch文が乱立してバグだらけに - 入力処理を直書きしたせいでキー変更が大変になった
- GameManagerを呼びたいのにどこからでも呼べず困った
これらはすべて、設計の「型(パターン)」を知らないことで起こる問題です。
本記事では、C++ゲーム開発で特に役立つ4大デザインパターン(Singleton・Factory・State・Command)を実装例とともに解説します。
デザインパターンとは?
デザインパターンとは、「ソフトウェア設計でよく直面する問題に対する、再利用可能な解決策のテンプレート」です。1994年にGoF(Gang of Four)が23パターンを体系化しました。
「特定の言語でしか使えない」ものではなく、C++、Java、C#、どの言語でも応用できる考え方です。ゲーム開発書の名著『Game Programming Patterns』でも、C++を例にパターンの活用法が詳しく解説されています。
本記事ではゲーム開発で特に登場頻度が高い4つに絞って解説します。プロトタイプ宣言を活用しながら読み進めると理解が深まります。
① Singletonパターン|「1つだけ存在する管理クラス」を安全に作る
どんなときに使う?
ゲーム全体で1つだけ存在すべき管理クラスに使います。例:GameManager、SoundManager、ScoreManager など。
▼ GameManager.h
#pragma once
#include <iostream>
class GameManager {
public:
// 唯一のインスタンスを返す
static GameManager& GetInstance() {
static GameManager instance;
return instance;
}
void SetScore(int s) { m_score = s; }
int GetScore() const { return m_score; }
private:
GameManager() : m_score(0) {} // コンストラクタを非公開
GameManager(const GameManager&) = delete;
GameManager& operator=(const GameManager&) = delete;
int m_score;
};▼ main.cpp(使用例)
#include "GameManager.h"
int main() {
GameManager::GetInstance().SetScore(500);
std::cout << "Score: " << GameManager::GetInstance().GetScore() << "\n";
return 0;
}▼ 実行結果
Score: 500
⚠️ 注意:Singletonは乱用するとコード全体が密結合になるアンチパターンにもなり得ます。「本当に1つだけでいいか?」を毎回問い直す習慣をつけましょう。どうしてもテストしづらくなる場合は、extern による共有や依存性注入(DI)も検討してください。
② Factoryパターン|「敵の生成」をスッキリまとめる
どんなときに使う?
敵キャラを増やすたびに new Goblin()、new Dragon() と書いていくと、呼び出し元のコードが種類ごとの分岐だらけになります。Factoryパターンは「生成処理を1か所に集約」することで、この問題を解決します。
▼ Enemy.h
#pragma once
#include <iostream>
#include <string>
// 敵の基底クラス
class Enemy {
public:
virtual ~Enemy() = default;
virtual void Attack() = 0;
virtual std::string GetName() const = 0;
};
// スライム
class Slime : public Enemy {
public:
void Attack() override { std::cout << "[スライム] 体当たり!\n"; }
std::string GetName() const override { return "Slime"; }
};
// ゴブリン
class Goblin : public Enemy {
public:
void Attack() override { std::cout << "[ゴブリン] 剣で斬りかかる!\n"; }
std::string GetName() const override { return "Goblin"; }
};▼ EnemyFactory.h
#pragma once
#include "Enemy.h"
#include <memory>
#include <string>
class EnemyFactory {
public:
// 敵の種類名からインスタンスを生成して返す
static std::unique_ptr<Enemy> Create(const std::string& type) {
if (type == "slime") return std::make_unique<Slime>();
if (type == "goblin") return std::make_unique<Goblin>();
return nullptr;
}
};▼ main.cpp(使用例)
#include "EnemyFactory.h"
int main() {
auto e1 = EnemyFactory::Create("slime");
auto e2 = EnemyFactory::Create("goblin");
e1->Attack();
e2->Attack();
std::cout << "生成した敵: " << e1->GetName() << ", " << e2->GetName() << "\n";
return 0;
}▼ 実行結果
[スライム] 体当たり!
[ゴブリン] 剣で斬りかかる!
生成した敵: Slime, Goblin
新しい敵を追加するときは EnemyFactory::Create に1行追加するだけでOK。呼び出し元のコードを一切変えずに拡張できます。staticメンバ関数との組み合わせも非常に相性が良いです。
③ Stateパターン|「シーン遷移・キャラ状態」をすっきり管理する
どんなときに使う?
ゲームには「タイトル→ゲーム中→ゲームオーバー」のような状態遷移が必ず存在します。これを if文 や switch文 で管理すると、状態が増えるほどコードが爆発します。Stateパターンは状態ごとにクラスを分けることで、この問題をエレガントに解決します。
▼ IScene.h(状態の基底クラス)
#pragma once
class SceneManager; // 前方宣言
class IScene {
public:
virtual ~IScene() = default;
virtual void Update(SceneManager& mgr) = 0;
virtual void Draw() = 0;
};▼ SceneManager.h
#pragma once
#include "IScene.h"
#include <memory>
class SceneManager {
public:
void ChangeScene(std::unique_ptr<IScene> next) {
m_next = std::move(next);
}
void Update() {
if (m_next) { // 遷移リクエストがあれば切り替え
m_current = std::move(m_next);
}
if (m_current) m_current->Update(*this);
}
void Draw() {
if (m_current) m_current->Draw();
}
private:
std::unique_ptr<IScene> m_current;
std::unique_ptr<IScene> m_next;
};▼ TitleScene.h / GameScene.h
#pragma once
#include "IScene.h"
#include "SceneManager.h"
#include <iostream>
// 前方宣言で循環インクルードを回避
class GameScene;
class TitleScene : public IScene {
public:
void Update(SceneManager& mgr) override {
std::cout << "[Title] Enterキーでゲーム開始\n";
// ここでは自動遷移(実際はキー入力で)
mgr.ChangeScene(std::make_unique<GameScene>());
}
void Draw() override { std::cout << "[Title] タイトル描画\n"; }
};
class GameScene : public IScene {
public:
void Update(SceneManager& mgr) override {
std::cout << "[Game] ゲームプレイ中...\n";
}
void Draw() override { std::cout << "[Game] ゲーム画面描画\n"; }
};▼ main.cpp(使用例)
#include "SceneManager.h"
#include "TitleScene.h"
int main() {
SceneManager mgr;
mgr.ChangeScene(std::make_unique<TitleScene>());
// 2フレーム分のループを擬似再現
for (int i = 0; i < 2; ++i) {
mgr.Update();
mgr.Draw();
std::cout << "---\n";
}
return 0;
}▼ 実行結果
[Title] Enterキーでゲーム開始
[Title] タイトル描画
---
[Game] ゲームプレイ中...
[Game] ゲーム画面描画
---
シーンを追加する場合は IScene を継承した新クラスを作るだけ。SceneManagerも既存シーンも一切変更不要です。
④ Commandパターン|「入力処理」を柔軟にしてUndoも実現
どんなときに使う?
キーとアクションを直接紐付けると、キーコンフィグの変更やリプレイ機能・Undo機能の実装が非常に難しくなります。Commandパターンは「操作をオブジェクト化」することでこれを解決します。
▼ ICommand.h
#pragma once
class ICommand {
public:
virtual ~ICommand() = default;
virtual void Execute() = 0;
virtual void Undo() = 0;
};▼ MoveCommand.h
#pragma once
#include "ICommand.h"
#include <iostream>
class Player;
class MoveCommand : public ICommand {
public:
MoveCommand(Player& player, int dx, int dy)
: m_player(player), m_dx(dx), m_dy(dy) {}
void Execute() override;
void Undo() override;
private:
Player& m_player;
int m_dx, m_dy;
};▼ Player.h / MoveCommand.cpp
// Player.h
#pragma once
#include <iostream>
class Player {
public:
int x = 0, y = 0;
void Move(int dx, int dy) {
x += dx; y += dy;
std::cout << "Player moved to (" << x << ", " << y << ")\n";
}
};
// MoveCommand.cpp
#include "MoveCommand.h"
#include "Player.h"
void MoveCommand::Execute() { m_player.Move( m_dx, m_dy); }
void MoveCommand::Undo() { m_player.Move(-m_dx, -m_dy); }▼ main.cpp(使用例 / Undo付き)
#include "MoveCommand.h"
#include "Player.h"
#include <stack>
#include <memory>
int main() {
Player player;
std::stack<std::unique_ptr<ICommand>> history;
// 右に移動
auto cmd1 = std::make_unique<MoveCommand>(player, 1, 0);
cmd1->Execute();
history.push(std::move(cmd1));
// 上に移動
auto cmd2 = std::make_unique<MoveCommand>(player, 0, 1);
cmd2->Execute();
history.push(std::move(cmd2));
// Undo(上に移動を取り消し)
std::cout << "--- Undo ---\n";
history.top()->Undo();
history.pop();
return 0;
}▼ 実行結果
Player moved to (1, 0)
Player moved to (1, 1)
--- Undo ---
Player moved to (1, 0)
stack に操作を積んでいくだけでUndo機能が完成します。さらに ICommand の実装クラスを増やせば、キーコンフィグ・リプレイ・チュートリアルの自動再生にも応用できます。
【体験談】パターンを知らずに作って地獄を見た話
チーム制作で2Dアクションゲームを作っていたとき、私はシーン遷移を int g_scene というグローバル変数で管理していました。
// ❌ こんなコードが至る所に…
if (g_scene == 0) { UpdateTitle(); }
else if (g_scene == 1) { UpdateGame(); }
else if (g_scene == 2) { UpdateGameOver(); }最初は3シーンだったのが、チームメンバーが「スタッフロール」「設定画面」「ショップ画面」を追加したことで、g_scene の値が 0〜7 まで増殖。どの値がどのシーンか誰も把握できなくなり、以下のようなエラーが頻発しました。
// 実際に起きたバグ
// g_scene = 5 のとき UpdateGame() が呼ばれてしまう
// → 条件分岐の順番ミス
Stateパターンに切り替えてからは、新シーンの追加はクラスを1つ作るだけになり、既存コードへの影響がゼロになりました。「もっと早く知りたかった」と心底思いました。externを使ったグローバル変数の共有も便利ですが、状態管理には向いていないと痛感しました。
よくある失敗例と対処法
❌ 失敗① Singletonを乱用してテスト不可能になる
症状:Singletonを多用すると、クラス間の依存が隠れてユニットテストができなくなる。
// 問題のあるコード例
void SomeClass::DoSomething() {
// どこからでもアクセスできるが、テスト時に差し替えができない
GameManager::GetInstance().AddScore(10);
}
対処法:本当に1つだけ必要なクラス(GameManager、SoundManager)に限定する。小さなクラスには extern や引数渡しで対応する。
❌ 失敗② Factoryで nullptr を返したまま使ってクラッシュ
症状:存在しない型名を渡すと nullptr が返り、Attack() 呼び出しでアクセス違反が発生する。
// エラー例
auto e = EnemyFactory::Create("dragon"); // 未登録の種類
e->Attack(); // ← e が nullptr のためクラッシュ
// Unhandled exception: Access violation
対処法:Create の戻り値を必ずチェックするか、デフォルト敵を返す安全な設計にする。
auto e = EnemyFactory::Create("dragon");
if (!e) {
std::cout << "[警告] 未知の敵タイプです\n";
return 1;
}
e->Attack();❌ 失敗③ Stateパターンで「遷移中に古いStateが Update を実行」してしまう
症状:ChangeScene を呼んだ直後に古いシーンの Update が続けて呼ばれ、二重処理やクラッシュが起きる。
対処法:上記の SceneManager のように、遷移リクエストを m_next に保存しておき、フレームの先頭で一括切り替えする設計にする。
void SceneManager::Update() {
// ✅ フレーム先頭でまず切り替えてからUpdateを呼ぶ
if (m_next) {
m_current = std::move(m_next);
}
if (m_current) m_current->Update(*this);
}4大パターン比較表
| パターン | 主な用途 | メリット | 注意点 |
|---|---|---|---|
| Singleton | GameManager, SoundManager | どこからでもアクセス可能 | 乱用すると密結合・テスト困難 |
| Factory | 敵・弾・アイテム生成 | 生成処理を1か所に集約 | nullptrのチェックを忘れずに |
| State | シーン遷移・キャラ状態 | 状態ごとにクラスが独立 | 遷移タイミングの管理に注意 |
| Command | 入力処理・Undo/Redo | 操作をオブジェクト化できる | コマンドクラスが増えやすい |
まとめ
- Singleton:ゲーム全体で1つだけ存在する管理クラスに使う。乱用は禁物
- Factory:敵・弾・アイテムなどの生成処理を1か所に集約し、拡張を容易にする
- State:シーン遷移やキャラ状態をクラスで分離し、if/switch地獄から解放される
- Command:入力処理をオブジェクト化することでUndo・キーコンフィグ・リプレイが実現できる
- 4つすべてを無理に使う必要はなく、「このコードが増えてきたら使う」という判断が大切
- パターンを知ることで「なぜこのコードが読みやすいか」も理解できるようになる
デザインパターンは覚えるものではなく、「問題が起きたときに思い出す引き出し」です。まずはStateパターンかFactoryパターンから実際のプロジェクトに取り入れてみましょう!

