【C++】デザインパターンをゲーム開発でうまく活用する方法|4大パターンを徹底解説

サムネイル画像 C++

ゲームを作り始めたころ、こんな経験はありませんか?

  • 敵キャラを増やすたびにコピペが増えて収拾がつかなくなった
  • シーン遷移に 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つだけ存在すべき管理クラスに使います。例:GameManagerSoundManagerScoreManager など。

▼ 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つだけ必要なクラス(GameManagerSoundManager)に限定する。小さなクラスには 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大パターン比較表

パターン主な用途メリット注意点
SingletonGameManager, SoundManagerどこからでもアクセス可能乱用すると密結合・テスト困難
Factory敵・弾・アイテム生成生成処理を1か所に集約nullptrのチェックを忘れずに
Stateシーン遷移・キャラ状態状態ごとにクラスが独立遷移タイミングの管理に注意
Command入力処理・Undo/Redo操作をオブジェクト化できるコマンドクラスが増えやすい

まとめ

  • Singletonゲーム全体で1つだけ存在する管理クラスに使う。乱用は禁物
  • Factory:敵・弾・アイテムなどの生成処理を1か所に集約し、拡張を容易にする
  • State:シーン遷移やキャラ状態をクラスで分離し、if/switch地獄から解放される
  • Command:入力処理をオブジェクト化することでUndo・キーコンフィグ・リプレイが実現できる
  • 4つすべてを無理に使う必要はなく、「このコードが増えてきたら使う」という判断が大切
  • パターンを知ることで「なぜこのコードが読みやすいか」も理解できるようになる

デザインパターンは覚えるものではなく、「問題が起きたときに思い出す引き出し」です。まずはStateパターンかFactoryパターンから実際のプロジェクトに取り入れてみましょう!


関連記事