2 / 4 ゲームループの基本構造
Game::Run()に実装されたゲームループの仕組みと、1フレーム内の処理順序を理解します。
ゲームループとは何か
一般的なアプリケーションと違い、アクションゲームやシューティングゲームでは、操作していない間も時間が進みます。キャラクターの移動やアニメーションも続きます。
これを実現するために、ゲームプログラムは以下の3つの段階を1秒間に何十回も高速で繰り返します。
| 段階 | 主な処理内容 | 目的 |
|---|---|---|
| 1. 入力 | キーボードやパッドの入力状態を調べる | このフレームでプレイヤーが何を押したかを記録する |
| 2. 更新 | 入力や時間経過に応じて状態を計算する | キャラクターの位置、速度、弾の発生などを更新する |
| 3. 描画 | 計算された最新の状態を画面へ描画する | プレイヤーの目に見える映像として出力する |
この処理の繰り返しをゲームループと呼びます。
この教材では、入力・更新・描画の1周を1フレームとし、1秒間に60回繰り返す60FPSを目標にします。ディスプレイが画面を更新する回数と、プログラムが処理を繰り返す回数は別です。
ゲームループ開始前の初期化処理
ゲームループを開始する前に、必要な画像素材の読み込みやオブジェクトの初期配置を行っておく必要があります。Game::Run()の冒頭では、次の処理が実行されます。
const ImageManager::LoadResult imageResult = images.LoadAll();
if (!imageResult.succeeded)
{
ShowResourceLoadError(L"画像", imageResult.failedPath);
return false;
}
player.Reset();画像の読み込みに失敗したら、エラーメッセージを表示して直ちに処理を中断します。必要な素材がそろわないまま開始しないことは、予期せぬクラッシュを防ぐために重要です。すべての素材を読み込めた場合だけplayer.Reset()で初期位置を設定し、ゲームループへ進みます。
ゲームループの実装と処理順序
Game::Run()の中心部分は、次のwhileループです。これは読むための抜粋で、末尾の待機処理を省略しています。配布済みのGame.cppは書き換えず、入力・更新・描画の呼び出しを対応させてください。
while (ProcessMessage() == 0)
{
input.Update();
if (input.IsDown(Input::Action::Exit))
break;
Update(config::DeltaTime);
ClearDrawScreen();
Draw();
ScreenFlip();
// 60FPSを維持するための待機処理
}このループ内で実行されている各処理の順序と役割は次のとおりです。
| 順序 | 実行される関数 | 処理の役割 |
|---|---|---|
| 1 | ProcessMessage() | Windowsからの閉じるボタン操作などを検知する |
| 2 | input.Update() | 現在のフレームにおけるすべてのキー入力状態を取得・保存する |
| 3 | Update(config::DeltaTime) | 固定の1/60秒と入力をもとに、ゲーム内の状態を更新する |
| 4 | ClearDrawScreen() | 直前のフレームで描いた古い画面を消去する |
| 5 | Draw() | 最新の状態をもとに、裏画面(非表示画面)へオブジェクトを描き込む |
| 6 | ScreenFlip() | 描き終わった裏画面を表画面へ瞬時に切り替えて表示する(ダブルバッファリング) |
| 7 | フレーム待機 | 次の更新予定時刻までの残り時間だけ待機する |
処理順序を守る理由
これらの処理順序が前後すると、ゲームの挙動に違和感が生じます。
たとえばDraw()をUpdate()より先に実行すると、1フレーム前の位置が描かれます。そのため、操作に対する画面の反応も1フレーム遅れます。ClearDrawScreen()を忘れると、前のフレームの残像が残り続けます。
リアルタイムゲームでは、「入力を受け取る → 状態を更新する → 最新状態を描く」という因果関係の順序を厳密に保つことが重要です。
Gameクラスの責務と委譲
ゲームループを統括するGameクラスは、すべての処理を自分自身で直接計算するわけではありません。
// ゲーム全体の更新処理
void Game::Update(float deltaTime)
{
player.Update(input, deltaTime);
}
// ゲーム全体の描画処理
void Game::Draw() const
{
player.Draw(images);
}Gameクラスの主な役割は、1フレーム内で処理を呼び出す順序を管理することです。移動の計算はPlayer::Update()、機体の描画はPlayer::Draw()で行います。このように個別の処理を担当クラスへ任せることを委譲と呼びます。
各クラスで処理を分担すると、Gameクラスが大きく複雑になるのを防ぎ、処理の流れを追いやすくできます。
1フレームの経過時間(デルタ時間)
この教材では、60FPS(1秒間に60フレーム)を基準として動作させます。1フレーム分の微小な経過時間はApplicationConfig.hに定数として定義されています。
inline constexpr int FrameRate = 60;
inline constexpr float DeltaTime = 1.0f / FrameRate;1回の更新で進める時間を デルタ時間(DeltaTime) と呼びます。このコードでは実際にかかった時間を測って渡すのではなく、毎回
速度を「1秒あたりに進むピクセル数」で表し、DeltaTimeを掛けると、1フレーム分の移動距離を計算できます。
固定デルタ時間と処理落ち
配布コードは処理にかかった時間を差し引いて待ちます。処理が予定時刻を超えた場合は待たずに次へ進みますが、遅れた分の更新をまとめて実行する仕組みはありません。毎秒30回しか更新できない状態では、ゲーム内で進む時間も実時間の半分になります。
- ゲームループ(入力 → 更新 → 描画)の処理順序とそれぞれの目的を説明できる
- ダブルバッファリング(
ClearDrawScreen→Draw→ScreenFlip)の仕組みを理解している -
Gameクラスが全体の進行を統括し、個別の処理を専門クラスへ委譲していることを説明できる - デルタ時間(
DeltaTime)の意味と、速度計算に用いる理由を説明できる