Skip to content
第01回 プレイヤーの表示と操作

2 / 4 ゲームループの基本

このステップの目的

Game::Run()に実装された基本的なゲームループを理解する

ゲームループとは

ゲームはプレイヤーからの入力と対話的に動作するソフトウェアです。
一般に、ゲームは以下のような処理を繰り返すことで動作します。

1. 入力2. 更新3. 描画
キー入力を受け付ける入力をゲーム内の状態へ反映する現在の状態を描画する

こうした基本的な動作のループを、この授業ではゲームループと呼びます。

コンピューターに接続されたディスプレイは1秒間に60回程度画面を更新します。 この更新単位をフレームと呼びます。

描画のタイミングに合わせてゲームループを繰り返すことで、プレイヤーは操作と画面の変化をリアルタイムに体験できます。

ゲームループを開始する前の初期化

Game::Run()は、最初に画像を読み込みます。

Game.cppcpp
const ImageManager::LoadResult imageResult = images.LoadAll();
if (!imageResult.succeeded)
{
    ShowResourceLoadError(L"画像", imageResult.failedPath);
    return false;
}

player.Reset();

読み込みに失敗した場合はShowResourceLoadErrorが対象ファイルのパスを表示し、ゲームを開始せずに終了します。 成功した場合はplayer.Reset()を呼びだしてプレイヤーを初期化した後、ゲームループを始めます。

ゲームループの実際

Game::Run()の中心となっているのは、次のwhile文です。

Game.cppcpp
while (ProcessMessage() == 0)
{
    input.Update();

    if (input.IsDown(Input::Action::Exit))
        break;

    Update(config::DeltaTime);
    ClearDrawScreen();
    Draw();
    ScreenFlip();

    // 次のフレームまで待つ処理
}

先のゲームループをより細分化した形で実装されています 処理の順序をまとめると次のようになります。

順序処理意味
1ProcessMessage()Windowsからの終了操作などを処理する
2input.Update()このフレームのキー入力を保存する
3Update()入力と1フレーム分の時間からゲーム内の状態を変える
4ClearDrawScreen()前のフレームの描画を消す
5Draw()現在の状態を裏画面へ描く
6ScreenFlip()描き終わった裏画面を表示する
7フレーム待機次のフレームを始める時刻まで待つ

これらの処理の順番は、入れ替えても致命的な問題は起きません。 ですが、例えばinput.Update()Update()より後に置くと、プレイヤーは前のフレームの入力を使って移動することになります。 また、Draw()Update()より前に置くと、移動前の位置が表示されることになります。

いずれも1フレーム分の遅延が起きるにすぎませんが、わずかながらもゲームの操作感や衝突判定の結果に影響します。 こうした機序に配慮し丁寧に実装することは、ゲームのようなリアルタイム性の高いソフトウェアでは極めて重要です。

Gameクラスの役割

Gameクラスは、ゲームの更新と描画の呼び出しを担当する、ゲームループの中心的なクラスです。 ですが自身では直接の更新や描画の処理は行いません。 プレイヤーの更新と描画の処理は包含するPlayerクラスに依頼します。

Game.cppcpp
// ゲームの更新
void Game::Update(float deltaTime)
{
    // プレイヤーの更新
    player.Update(input, deltaTime);
}

// ゲームの描画
void Game::Draw() const
{
    // プレイヤーの描画
    player.Draw(images);
}

Gameは所有している入力をPlayer::Update()に、画像管理の情報をPlayer::Draw()へ渡しているだけです。
Playerは自身の責任で、自分の移動と表示に集中します。

Gameは各クラスの処理を抱え込まず、1フレームの処理順を取りまとめます。 ですが同時に、Gameは各クラスの処理を呼び出す責任も持っています。

Gameクラスの責任

Gameはゲーム本体を表すクラスです。 ゲームに必要なオブジェクトの寿命の管理にも責任を持ちます。 今回はゲーム進行に必須なPlayerを所有しています。 この先の授業で弾や敵を追加するときも、Gameはそれらのオブジェクトを所有し、更新と描画の呼び出しを行います。

第02回以降に弾や敵を追加するときも、Gameは直接処理を行わず、担当するクラスに依頼します。

1フレームと時間

この教材では60FPSを基準にし、1フレーム分の時間を1/60秒として扱います。 値はApplicationConfig.hに定義されています。

ApplicationConfig.hcpp
inline constexpr int FrameRate = 60;
inline constexpr float DeltaTime = 1.0f / FrameRate;

こうした微小な経過時間をデルタ時間と呼びます。 ここでは1フレーム分の時間をDeltaTimeとして定数化し、ゲーム内の移動速度やアニメーションの速度の計算に利用します。

60FPSで動作している場合、DeltaTimeは約0.0167秒です。 1秒あたりの速度にDeltaTimeを掛けると、1フレーム分の移動量を求めることができます。

フレーム待機の仕組み

Game::Run()は次に表示が発生する時刻をnextFrameに記録し、処理が早く終わった分だけ待機します。

処理時間の小数分を次のフレームへ持ち越すため、毎回同じ整数ミリ秒だけ待つより誤差が蓄積しにくくなっています。 処理が重く60FPSを維持できない場合も、DeltaTime自体は1/60秒の固定値です。 このため、処理が重い状態が続くとゲーム自体の進行が遅くなる、いわゆる処理落ちが発生します。

3Dゲームでは処理負荷を制御するのが難しいため、実際に経過した時間を測ってゲームの進行を制御する方式がよく使われます。 今回は簡単のため、固定値のDeltaTimeを使う方式を採用しています。

チェックリスト
  • ゲームループの基本的な構造を説明できる
  • ゲームのフレームとデルタ時間の意味を説明できる