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 Locality、ECS)
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では書けない(エンジンが所有)。自分で書くのはエンジン自作・組み込み・サーバーのとき
関連項目¶
理解度チェック¶
- 可変タイムステップで物理が壊れる理由を説明できますか。
- accumulator方式の固定ステップで、描画側の補間は何のためにありますか。
- UnityのUpdateとFixedUpdateはこのページのどの方式に対応しますか。
演習¶
検証サンプル(samples/game_loop.cpp)の固定ステップループに「更新消化回数の上限」を追加し、重い処理(人工的なスリープ)を入れても死のスパイラルに陥らないことを確認してください。
前: カテゴリ目次 | 次: Component と ECS