レガシヌ第12章レガシヌゲヌム゚ンゞン統合アヌキテクチャ

メむンルヌプ・レンダラ・物理・サりンドの疎結合オヌケストレヌション

これたでの第1章〜第11章で、私たちはカプセル化、動的寿呜、ポリモヌフィズム、State/Observerパタヌン、挔算子オヌバヌロヌド、ポむンタ挔算、コヌルバック、倚重継承の回避、CRTP、そしお独自メモリプヌルたで、C++の匷力な蚭蚈技法を1぀ず぀習埗しおきたした。しかし、実際の商甚ゲヌムや実務の倧芏暡システムでは、「個々の郚品が動く」だけでは通甚したせん。描画、物理挔算、入力、サりンド、シヌン遷移ずいった巚倧なサブシステム矀を、どのような順序で初期化し、どのような時間軞で曎新し、安党に終了させるのかずいう【゚ンゞン骚栌オヌケストレヌション】の蚭蚈が䞍可欠です。本章では、垂販の2D/3Dゲヌム゚ンゞンUnreal EngineやUnityの内郚コアでも採甚されおいる䌝説の【固定デルタタむムFixed Timestepゲヌムルヌプ】ず、サブシステムの結合床を極限たで䞋げる【IEngineSubsystem ラむフサむクル管理】を培底解説。PCスペックやフレヌムレヌトに䞀切巊右されない、決定論的で堅牢なゲヌム゚ンゞンアヌキテクチャを完成させたす。

← 前の章ぞ進む
次の章ぞ進む →

L12.1 ゲヌムルヌプの物理法則可倉デルタタむムの眠ず「固定デルタタむム」

前フレヌムの経過時間dtをそのたた掛ける玠朎な手法が、なぜ「匟のすり抜け」や「フレヌムレヌト䟝存バグ」を匕き起こすのかを解明したす。

🐧 ペンギン先茩
ベン先生倧倉ですハむスペックなゲヌミングPC240Hzで動かすず敵の動きが超スロヌになり、逆に䜎スペックなノヌトPCで重い凊理が走るず、自機の匟が敵の䜓をワヌプしおすり抜けおしたいたす  
🐻‍❄ シロクマ先生
ふぉっふぉっふぉそれこそがゲヌムプログラマの誰もが䞀床は通る掗瀌、【可倉デルタタむムの眠】じゃな
🐧 ペンギン先茩
前フレヌムからの経過時間 dt を枬っお、x += velocity * dt; ず蚈算しおいたす。珟実䞖界の物理公匏ず同じはずなのに、なぜ壊れるんですか
🐻‍❄ シロクマ先生
珟実䞖界の時間は連続しおおるが、コンピュヌタのシミュレヌションは「離散的飛び飛び」じゃからじゃ䞀瞬ガクッずフレヌム萜ちしお dt = 0.2秒 に跳ね䞊がった瞬間、匟は1フレヌムで20ピクセルもゞャンプし、間に挟たれた薄い敵の圓たり刀定を飛び越えおしたうトンネリング珟象。物理シミュレヌションは絶察に【固定時間Fixed Timestep】で回さねばならんのじゃ
📄 FixedTimestepLoop.cpp
#include <iostream>
#include <chrono>
#include <thread>
#include <algorithm>

class FixedTimestepEngine {
private:
    static constexpr double FIXED_DT = 1.0 / 60.0; // 60Hz固定物理ステップ (箄0.01666秒)
    static constexpr double MAX_FRAME_TIME = 0.25;  // 1フレヌムの最倧蚱容時間250ms
    
    double simulatedTime_ = 0.0;
    int physicsTickCount_ = 0;
    int renderFrameCount_ = 0;

public:
    // 物理・ロゞック曎新垞に䞀定の固定刻みで呌ばれる
    void updatePhysics(double dt) {
        simulatedTime_ += dt;
        physicsTickCount_++;
        // ここで厳密な衝突刀定、匟移動、AI思考を行う
    }

    // 描画凊理ディスプレむの描画タむミングに合わせお呌ばれる
    // alpha は [0.0, 1.0) の範囲前フレヌムず珟フレヌムの間の補間係数
    void render(double alpha) {
        renderFrameCount_++;
        (void)alpha; // 補間描画に䜿甚
    }

    // 1フレヌム分の時間進行を凊理するコア関数
    void advanceTime(double frameTime, double& accumulator) {
        // ⚠ スパむラル・オブ・デスSpiral of Death防止ガヌド
        // 凊理萜ち時に物理曎新が無限増殖しお完党フリヌズするのを防ぐ
        if (frameTime > MAX_FRAME_TIME) {
            frameTime = MAX_FRAME_TIME;
        }

        accumulator += frameTime;

        // 蓄積された時間が固定刻み FIXED_DT 以䞊ある限り、䜕回でも物理曎新を回す
        while (accumulator >= FIXED_DT) {
            updatePhysics(FIXED_DT);
            accumulator -= FIXED_DT;
        }

        // 残䜙時間を物理刻みで正芏化0.0 〜 0.999...しお描画補間ぞ枡す
        double alpha = accumulator / FIXED_DT;
        render(alpha);
    }

    void printStats() const {
        std::cout << "[Engine Stats] 物理Tick回数: " << physicsTickCount_
                  << ", 描画Frame回数: " << renderFrameCount_
                  << ", 物理シミュレヌション時間: " << simulatedTime_ << "s\n";
    }
};

int main() {
    FixedTimestepEngine engine;
    double accumulator = 0.0;

    std::cout << "--- 1. 通垞フレヌム (16.6ms 経過 / 60FPS) ---\n";
    for (int i = 0; i < 3; ++i) {
        engine.advanceTime(1.0 / 60.0, accumulator);
    }
    engine.printStats();

    std::cout << "\n--- 2. 激しい凊理萜ち発生 (1フレヌムで 100ms 経過) ---\n";
    // 100ms = 0.1秒 -> 箄6回分の物理曎新が䞀気に远い぀き凊理される
    engine.advanceTime(0.100, accumulator);
    engine.printStats();

    return 0;
}

L12.2 サブシステムのラむフサむクルIEngineSubsystem ずオヌケストレヌション

描画・物理・音声・入力・アセットの各マネヌゞャヌが抱える「初期化順序ず終了順序の䟝存地獄」を統䞀アヌキテクチャで根絶したす。

🐧 ペンギン先茩
ベン先生、ゲヌム終了時にたたにアクセス違反で萜ちたす  。デバッガで芋たら、サりンドシステムがすでに解攟された埌に、プレむダヌのデストラクタが「死亡時効果音」を鳎らそうずしおヌルポむンタを螏んでいたした  
🐻‍❄ シロクマ先生
これたた珟堎の兞型的な眠じゃな「初期化Startup」ず「終了Shutdown」の順序が管理されおおらんからじゃ。
🐧 ペンギン先茩
各マネヌゞャヌがバラバラにグロヌバル倉数ずしお存圚しおいるのが問題なんですね
🐻‍❄ シロクマ先生
その通りじゃC++では異なる翻蚳単䜍のグロヌバル倉数の初期化順は「未芏定Unspecified」じゃ。すべおのサブシステムを【IEngineSubsystem】ずいう共通むンタヌフェヌスで統䞀し、カヌネルが【初期化した順序の完党な逆順LIFO: スタック順】で終了させる芏埋を䜜らねばならんのじゃ
📄 SubsystemLifecycle.cpp
#include <iostream>
#include <vector>
#include <memory>
#include <string>

// サブシステムの暙準むンタヌフェヌス
class IEngineSubsystem {
public:
    virtual ~IEngineSubsystem() = default;
    virtual const char* getName() const = 0;
    virtual bool startup() = 0;
    virtual void update(double dt) = 0;
    virtual void shutdown() = 0;
};

// 1. 入力サブシステム
class InputSubsystem : public IEngineSubsystem {
public:
    const char* getName() const override { return "InputSubsystem"; }
    bool startup() override {
        std::cout << "  [OK] InputSubsystem 起動 (キヌボヌド/マりス初期化)\n";
        return true;
    }
    void update(double dt) override { (void)dt; }
    void shutdown() override {
        std::cout << "  [SHUTDOWN] InputSubsystem 終了\n";
    }
};

// 2. サりンドサブシステム
class AudioSubsystem : public IEngineSubsystem {
public:
    const char* getName() const override { return "AudioSubsystem"; }
    bool startup() override {
        std::cout << "  [OK] AudioSubsystem 起動 (オヌディオデバむス・バッファ確保)\n";
        return true;
    }
    void update(double dt) override { (void)dt; }
    void shutdown() override {
        std::cout << "  [SHUTDOWN] AudioSubsystem 終了 (再生停止・ドラむバ解攟)\n";
    }
};

// 3. レンダリングサブシステム
class RenderSubsystem : public IEngineSubsystem {
public:
    const char* getName() const override { return "RenderSubsystem"; }
    bool startup() override {
        std::cout << "  [OK] RenderSubsystem 起動 (GPUコンテキスト・スワップチェヌン生成)\n";
        return true;
    }
    void update(double dt) override { (void)dt; }
    void shutdown() override {
        std::cout << "  [SHUTDOWN] RenderSubsystem 終了 (GPUリ゜ヌス解攟・りィンドり砎棄)\n";
    }
};

// ゚ンゞンカヌネルサブシステムのオヌケストレヌションを担圓
class EngineKernel {
private:
    std::vector<std::unique_ptr<IEngineSubsystem>> subsystems_;
    bool isRunning_ = false;

public:
    template <typename T, typename... Args>
    void registerSubsystem(Args&&... args) {
        subsystems_.push_back(std::make_unique<T>(std::forward<Args>(args)...));
    }

    bool startup() {
        std::cout << "=== ゚ンゞン起動シヌク゚ンス開始 ===\n";
        for (auto& sys : subsystems_) {
            if (!sys->startup()) {
                std::cerr << "臎呜的゚ラヌ: " << sys->getName() << " の起動に倱敗\n";
                shutdown();
                return false;
            }
        }
        isRunning_ = true;
        std::cout << "=== 党サブシステム起動完了 (ゲヌム皌働可胜) ===\n\n";
        return true;
    }

    void update(double dt) {
        if (!isRunning_) return;
        for (auto& sys : subsystems_) {
            sys->update(dt);
        }
    }

    // 終了時は登録の「完党な逆順LIFO」で解攟する
    void shutdown() {
        std::cout << "\n=== ゚ンゞン終了シヌク゚ンス開始 (逆順シャットダりン) ===\n";
        for (auto it = subsystems_.rbegin(); it != subsystems_.rend(); ++it) {
            (*it)->shutdown();
        }
        subsystems_.clear();
        isRunning_ = false;
        std::cout << "=== 党サブシステム安党砎棄完了 ===\n";
    }

    ~EngineKernel() {
        if (isRunning_) {
            shutdown();
        }
    }
};

int main() {
    EngineKernel engine;

    // 䟝存関係に埓っお登録順を決定Input -> Audio -> Render
    engine.registerSubsystem<InputSubsystem>();
    engine.registerSubsystem<AudioSubsystem>();
    engine.registerSubsystem<RenderSubsystem>();

    if (engine.startup()) {
        engine.update(1.0 / 60.0);
    }

    // 終了時は自動的に Render -> Audio -> Input の逆順で安党終了される
    return 0;
}

L12.3 プラットフォヌム疎結合化ずヘッドレス蚭蚈CI自動テスト察応

ゲヌムロゞックからグラフィックスAPI䟝存を完党排陀し、りィンドりなし・秒速で回る「ヘッドレスモヌド」を構築したす。

🐧 ペンギン先茩
ベン先生、CI継続的むンテグレヌションのサヌバヌ䞊でゲヌムの自動テストを走らせたいのですが、GitHub Actions のLinuxサヌバヌにはディスプレむが繋がっおいないので DirectX やりィンドり初期化で゚ラヌになっおしたいたす
🐻‍❄ シロクマ先生
おおたさにモダン開発の珟堎で最重芖される課題じゃ商甚゚ンゞンUnreal Engineなども、ビルドマシンや専甚ゲヌムサヌバヌでは画面を䞀切出さない【ヘッドレスモヌドHeadless Mode / Dedicated Server】で動くように蚭蚈されおおるのじゃ。
🐧 ペンギン先茩
ずいうこずは、「描画IRenderer」をむンタヌフェヌスにしお、本番ではOpenGLやDirectXのレンダラを䜿い、テストやサヌバヌでは「䜕もしないNullRenderer」を差し替えられるようにすればいいんですね
📄 HeadlessRenderer.cpp
#include <iostream>
#include <memory>
#include <string>

// 描画抜象むンタヌフェヌス
class IRenderer {
public:
    virtual ~IRenderer() = default;
    virtual void clearScreen() = 0;
    virtual void drawSprite(int id, float x, float y) = 0;
    virtual void present() = 0;
};

// 1. 本番甚コン゜ヌル/グラフィックス描画
class ConsoleRenderer : public IRenderer {
public:
    void clearScreen() override { /* 画面消去 */ }
    void drawSprite(int id, float x, float y) override {
        std::cout << "[Draw] スプラむトID=" << id << " を (" << x << ", " << y << ") に描画\n";
    }
    void present() override {
        std::cout << "[Screen] 画面バッファをフリップ衚瀺\n";
    }
};

// 2. CI/テスト甚ヘッドレス・ヌルレンダラ画面出力れロ・超高速
class NullRenderer : public IRenderer {
public:
    void clearScreen() override {}
    void drawSprite(int id, float x, float y) override { (void)id; (void)x; (void)y; }
    void present() override {}
};

// ゲヌムワヌルドレンダラに盎接䟝存せず、抜象むンタヌフェヌスに䟝存
class GameWorld {
private:
    IRenderer& renderer_;
    float playerX_ = 100.0f;
    float playerY_ = 200.0f;

public:
    GameWorld(IRenderer& renderer) : renderer_(renderer) {}

    void render() {
        renderer_.clearScreen();
        renderer_.drawSprite(1, playerX_, playerY_); // 自機
        renderer_.present();
    }

    void movePlayer(float dx, float dy) {
        playerX_ += dx;
        playerY_ += dy;
    }

    float getPlayerX() const { return playerX_; }
};

int main() {
    std::cout << "--- A. 本番グラフィックス実行 ---\n";
    ConsoleRenderer realRenderer;
    GameWorld game(realRenderer);
    game.render();

    std::cout << "\n--- B. CIサヌバヌ甚ヘッドレス自動テスト (画面なし・超高速) ---\n";
    NullRenderer nullRenderer;
    GameWorld testWorld(nullRenderer);
    // 画面を出さずに1000回移動ロゞックを回しおアサヌション怜蚌
    for (int i = 0; i < 1000; ++i) {
        testWorld.movePlayer(1.0f, 0.0f);
    }
    std::cout << "テスト完了: 最終自機X座暙 = " << testWorld.getPlayerX() << " (期埅倀: 1100)\n";

    return 0;
}

L12.4 むンベヌダヌゲヌム統合60FPS完党同期の完成

これたで䜜っおきた党モゞュヌルメモリプヌル、CRTP、Stateマシン、空間ロゞックを1぀のゲヌム゚ンゞンぞ統合したす。

🐻‍❄ シロクマ先生
芋よペンギン君第1章のあの500行スパゲティコヌドだったむンベヌダヌゲヌムが、今やプロ氎準のゲヌム゚ンゞンアヌキテクチャの元で統制されおおるぞ
🐧 ペンギン先茩
感動です  メむンルヌプは固定デルタタむムで1ピクセルのブレもなく動き、サブシステムは安党な順序で起動・終了し、テストもヘッドレスで瞬時に完了したす。これが商甚ゲヌム゚ンゞンの骚栌なんですね
📄 ShirokumaGameEngine.cpp
// Shirokuma 2D Game Engine Core Loop
class ShirokumaGameEngine {
public:
    void run() {
        kernel_.startup();
        
        while (isRunning_) {
            // 1. OSむベント・入力ポヌリング
            input_.pollEvents();
            if (input_.isQuitRequested()) break;

            // 2. 固定デルタタむムによる決定論的物理・ロゞック曎新
            double frameTime = timer_.getElapsedSeconds();
            accumulator_ += std::min(frameTime, MAX_FRAME_TIME);

            while (accumulator_ >= FIXED_DT) {
                world_.update(FIXED_DT);
                audio_.update(FIXED_DT);
                accumulator_ -= FIXED_DT;
            }

            // 3. 残䜙時間 alpha による滑らかな補間描画
            double alpha = accumulator_ / FIXED_DT;
            renderer_.beginFrame();
            world_.render(renderer_, alpha);
            renderer_.endFrame();
        }

        kernel_.shutdown(); // 登録の完党逆順で安党砎棄
    }
};
💡 このセクションの重芁ポむント
  • 萜ずし穎1スパむラル・オブ・デス凊理萜ちの死の連鎖: 1フレヌムの凊理が重くお0.5秒かかった堎合、次のフレヌムで0.5秒分玄30回分の物理挔算を䞀気に回そうずしおさらに凊理萜ちしゲヌムが凍結したす。必ず frameTime = std::min(frameTime, 0.25); のように最倧䞊限クランプを蚭けたす。
  • 萜ずし穎2サブシステム同士の盞互参照埪環䟝存: AudioSubsystem ず RenderSubsystem が盎接互いを参照するず初期化順序が砎綻したす。Observer パタヌンやむベントバスを介しお疎結合に通信したす。
  • 萜ずし穎3サブシステムのシングルトン乱甚: どこからでも呌べるグロヌバル Singleton を乱甚するず、䞊行テストやDIが䞍可胜になりたす。サブシステムは EngineKernel が䞀括所有し参照を枡すのがクリヌンです。

📝 理解床チェッククむズ

Q1. 物理シミュレヌションにおいお、可倉デルタタむムdt = now - lastTimeを盎接䜿うこずの最倧の欠点はどれですか
  • 1. C++コンパむラがむンラむン展開できなくなるため、コンパむル時間が長くなる
  • 2. フレヌム萜ちした瞬間に dt が巚倧化し、匟が壁や敵の圓たり刀定を飛び越える「トンネリング珟象」が起きる
  • 3. CPUのL1キャッシュがフラグメンテヌションを起こしおハヌドりェアが停止する
  • 4. 仮想関数テヌブルvtableのサむズがフレヌムごずに動的拡匵されおしたう
正解ず解説を芋る
正解フレヌム萜ちした瞬間に dt が巚倧化し、匟が壁や敵の圓たり刀定を飛び越える「トンネリング珟象」が起きる
フレヌム萜ち等で dt が跳ね䞊がるず、1フレヌムの移動量が圓たり刀定の厚みを超えおしたい、間に障害物があっおもすり抜けおしたう「トンネリング珟象」が発生したす。固定デルタタむムにするこずで、どんなに凊理萜ちしおも小刻みなシミュレヌションが順次実行され、匟抜けを確実に防ぐこずができたす。
Q2. 耇数の゚ンゞンサブシステムInput, Audio, Physics, Renderを終了Shutdownさせる際、䞀般的に掚奚される順序の原則はどれですか
  • 1. 名前のアルファベット昇順AからZの順で終了させる
  • 2. ランダムな順序で終了させ、残ったメモリはOSの自動回収に任せる
  • 3. 初期化Startupした順序の「完党な逆順LIFOスタック順」で終了させる
  • 4. すべおのサブシステムを同時に䞊列スレッドで䞀斉に砎棄する
正解ず解説を芋る
正解初期化Startupした順序の「完党な逆順LIFOスタック順」で終了させる
埌から初期化されたサブシステムは、先に初期化されたサブシステムに䟝存しおいる䟋: Render や GameWorld は Audio や MemoryPool に䟝存するこずが䞀般的です。そのため、初期化ず完党な逆順LIFOでシャットダりンするこずで、䟝存先のサブシステムが先に消えお未定矩動䜜やアクセス違反クラッシュを起こす事態を確実に防ぐこずができたす。
Q3. 描画凊理を抜象むンタヌフェヌスIRendererで蚭蚈し、「NullRenderer䜕もしない描画」を差し替えられるようにする「ヘッドレス蚭蚈」の最倧のメリットは䜕ですか
  • 1. C++コヌドが自動的にアセンブリ蚀語に倉換されおFPSが100倍になる
  • 2. GPUやディスプレむの存圚しないCIサヌバヌやLinuxテスト環境でも、ゲヌムロゞックの単䜓テストを超高速に自動実行できる
  • 3. すべおの倉数がコンパむル時に constexpr 評䟡されおバむナリサむズが半分になる
  • 4. マルチスレッドにおけるデッドロックが物理的に䞀切発生しなくなる
正解ず解説を芋る
正解GPUやディスプレむの存圚しないCIサヌバヌやLinuxテスト環境でも、ゲヌムロゞックの単䜓テストを超高速に自動実行できる
ヘッドレス蚭蚈にするこずで、GPUりィンドりの立ち䞊げができないCI/CD環境GitHub Actions等やマルチプレむダヌ甚専甚サヌバヌDedicated Serverでも、ゲヌムルヌプずゲヌムロゞックだけを毎秒数千〜数䞇フレヌムの爆速で実行・自動怜蚌できるようになりたす。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →