コンテンツにスキップ

Game Loop と Update Method

一言で言うと

  • Game Loop: 「入力を読み→世界を更新し→描画する」を回し続ける、ゲームの心臓部。時間の進め方を決めるパターン。
  • Update Method: ループの中で、各オブジェクトに「1フレーム分の自分の処理」(Update(dt))をさせるパターン。UnityのUpdate()の正体。

解決したい問題

通常のプログラムは「入力を待って」処理します。ゲームは誰も操作していなくても世界が動き続ける必要があり、しかも実行マシンの速度に関係なく同じゲーム速度でなければなりません。

最も単純な例(問題のある実装)

// C++20(問題例)— 回るだけのループ
while (running) {
    ProcessInput();
    Update();      // 1回で世界を1ステップ進める
    Render();
}
// 速いPCでは敵が高速化し、遅いPCではスローモーションになる
// (ステップがフレーム数に固定されているため)

古いゲームがこの作りで、高速な現代CPUで動かすと超高速になります。

改善1: 可変タイムステップ

// C++20 — 前フレームからの経過時間で世界を進める
auto prev = Clock::now();
while (running) {
    auto now = Clock::now();
    float dt = DurationSec(now - prev);   // 経過秒
    prev = now;
    ProcessInput();
    Update(dt);        // 移動は speed * dt のように書く
    Render();
}

実時間に追従するのでゲーム速度は安定。しかし dt が毎回違うため、物理や当たり判定の結果がフレームレート依存になります(dtが大きいと壁を突き抜ける等)。浮動小数点の誤差蓄積も再現性を壊します。

改善2: 固定タイムステップ+補間(現代の標準形)

// C++20 — 更新は固定刻み、描画は可変。Gaffer on Games "Fix Your Timestep" の形
constexpr float kFixedDt = 1.0f / 60.0f;
float accumulator = 0.0f;
auto prev = Clock::now();
while (running) {
    auto now = Clock::now();
    accumulator += DurationSec(now - prev);
    prev = now;

    ProcessInput();
    while (accumulator >= kFixedDt) {   // 溜まった実時間を固定刻みで消化
        FixedUpdate(kFixedDt);          // 物理・ロジックは常に1/60秒刻み(決定的)
        accumulator -= kFixedDt;
    }
    Render(accumulator / kFixedDt);     // 端数は描画の補間に使う(カクつき防止)
}

検証済みサンプル: samples/game_loop.cpp

  • 物理・ロジックが決定的(同じ入力なら同じ結果 → リプレイ・ネット同期の前提)
  • 注意: 更新が1/60秒より遅いマシンでは accumulator が溜まり続ける「死のスパイラル」になるため、実務では消化回数に上限を設けます

Update Method

ループが直接全オブジェクトの処理を書くと神関数化するので、各オブジェクトに委ねます。

// C++20
class GameObject {
public:
    virtual ~GameObject() = default;
    virtual void Update(float dt) = 0;   // 「自分の1フレーム」を自分で書く
};
// ループ側: for (auto& obj : objects) obj->Update(dt);

これはTemplate Method的構造(ループが骨格、Updateがフック)+Composite的な一括処理です。

Update Methodの注意点

  • 更新順序: AがBの位置を参照するとき、Bが先に動いたか後かで結果が変わる。順序を保証しない設計(前フレームの値を読む → Double Buffer)か、明示的な順序付けが必要
  • 更新中のリスト変更: Update中にスポーン・削除するとイテレータ破壊(→ Iteratorの落とし穴)。追加・削除は遅延リストで
  • 数万オブジェクトの仮想Update呼び出しはキャッシュ・分岐に不利(→ Data LocalityECS)

Unity/C# との対応

  • Update() = 可変ステップ(Time.deltaTime)、FixedUpdate() = 固定ステップ(物理用)。「物理はFixedUpdate、見た目はUpdate」の使い分けはこのページの2方式そのもの
  • Time.timeScale はループの時間の進みを操作する仕組み(ポーズ・スロー演出)
  • MonoBehaviourのUpdateはリフレクションで収集され、C++側から一括呼び出しされる(1個あたりのオーバーヘッドがあるため、大量オブジェクトでは自前のManagerが束ねるテクニックがある)

Unreal Engine との対応

  • AActor::Tick(float DeltaSeconds) が可変ステップUpdate。Substepping 設定で物理は内部的に固定刻み化
  • Tickの実行順は TickGroup(PrePhysics/DuringPhysics/PostPhysics)で制御——更新順序問題への明示的な回答

使う場面 / 使わない場面

  • 固定ステップ: 物理、ネット同期、リプレイ、格闘・音ゲーなど決定性が要るもの
  • 可変ステップ: カメラ、UIアニメ、パーティクルなど見た目だけのもの
  • Update Methodを使わない場面: 毎フレーム処理が不要なオブジェクト(イベント駆動で十分な設置物)にまで空Updateを持たせない(呼び出しコストの無駄。Unityでは空Updateを消すだけで速くなる)

よくある誤解

  • 「deltaTimeを掛ければ全部フレームレート非依存になる」— 移動は近似的にそうなるが、加速度・減衰・当たり判定は可変dtでは厳密に一致しない。決定性が要るなら固定ステップ一択
  • 「ゲームループは自分で書くもの」— Unity/UEでは書けない(エンジンが所有)。自分で書くのはエンジン自作・組み込み・サーバーのとき

関連項目

理解度チェック

  1. 可変タイムステップで物理が壊れる理由を説明できますか。
  2. accumulator方式の固定ステップで、描画側の補間は何のためにありますか。
  3. UnityのUpdateとFixedUpdateはこのページのどの方式に対応しますか。

演習

検証サンプル(samples/game_loop.cpp)の固定ステップループに「更新消化回数の上限」を追加し、重い処理(人工的なスリープ)を入れても死のスパイラルに陥らないことを確認してください。


前: カテゴリ目次 | 次: Component と ECS