Skip to main content

第5部 ゲームの基本的な構成

第5部 ゲームの基本的な構成

このワークの目標

ゲームは、入力・更新・描画を何度も繰り返して動いています。

このワークでは、次の内容を確認します。

  1. ゲームループの処理順を説明できる
  2. Initialize()、Update()、Draw()、Release()の役割を区別できる
  3. タイトル・プレイ・クリア・ゲームオーバーの状態遷移を追跡できる
  4. 現在の状態に応じて、Update()とDraw()の処理を切り替えられる
  5. フレーム・FPS・デルタタイムの関係を説明し、移動量を計算できる

1.ゲームループ

1-1.ゲーム全体の基本形

ゲームは、最初から最後まで同じ処理を繰り返しているわけではありません。

Initialize();

while (ゲームを続ける)
{
    Update();
    Draw();
}

Release();

ゲームループの構成ゲームループの構成

図1 ゲーム開始から終了までの基本的な処理順

関数 実行する時期 主な役割
Initialize() ゲーム開始時に1回 変数の初期化、画像や音の読み込み
Update() 毎フレーム 入力の確認、移動、当たり判定、状態の変更
Draw() 毎フレーム 背景、キャラクター、文字などの描画
Release() ゲーム終了時に1回 読み込んだ画像や音などの解放

1-2.Input()はUpdate()の中で呼び出す

このワークでは、キーやコントローラーの入力を調べる Input() を、Update()の最初で呼び出します。

void Update()
{
    Input();

    // 入力結果を使って、ゲーム内の状態を更新する
}

入力は毎フレーム調べます。Initialize()で一度だけ調べても、その後に押されたキーは分かりません。

1-3.1フレームの処理

ゲームループの中を1回実行すると、1フレーム分の処理になります。

Update()
  ├─ Input()で入力を調べる
  └─ 座標やゲームの状態を更新する
        ↓
Draw()
  └─ 更新後の状態を画面へ描く

Draw()の後はゲームループの先頭へ戻り、次のフレームの Update()を実行します。


練習1 処理を正しい順番に並べよう

次の処理を、ゲーム開始から終了までの順番に並べよう。繰り返す部分は [ ] で囲もう。

  • Release()
  • Draw()
  • Initialize()
  • Update()
______ → [ ______ → ______ ] → ______
練習1の模範解答を見る
Initialize() → [ Update() → Draw() ] → Release()

Update()とDraw()を、ゲームが終了するまで繰り返します。


練習2 処理を分類しよう

次の処理を、最も適切な関数へ分類しよう。

処理 関数名
プレイヤー画像を読み込む ________
右キーが押されているか調べる ________
プレイヤーの座標を変える ________
プレイヤー画像を画面に表示する ________
読み込んだ画像を解放する ________
練習2の模範解答を見る
処理 関数名
プレイヤー画像を読み込む Initialize()
右キーが押されているか調べる Input()
プレイヤーの座標を変える Update()
プレイヤー画像を画面に表示する Draw()
読み込んだ画像を解放する Release()

このワークでは Input()を Update()の中から呼び出します。


練習3 ゲームループを完成させよう

空欄を埋めよう。

________();

while (ゲームを続ける)
{
    ________();
    ________();
}

________();
練習3の模範解答を見る
Initialize();

while (ゲームを続ける)
{
    Update();
    Draw();
}

Release();

2.ゲームシーケンス

2-1.ゲームを状態に分ける

ゲームの進行を、画面や役割ごとのまとまりに分けて考えます。このまとまりを、ここでは状態と呼びます。

このワークでは、次の4つの状態を扱います。

状態 内容
TITLE タイトル画面で開始操作を待つ
PLAY プレイヤーを操作してゲームを進める
CLEAR 目的を達成した結果を表示する
GAMEOVER 失敗した結果を表示する

現在の状態は、変数 state に保存します。

state = TITLE;

2-2.状態遷移

条件を満たして state の値が変わることを、状態遷移といいます。

ゲームシーケンスの状態遷移ゲームシーケンスの状態遷移

図2 タイトル・プレイ・クリア・ゲームオーバーの状態遷移

現在の状態 条件 次の状態
TITLE 開始ボタンを押した PLAY
PLAY ゴールした CLEAR
PLAY プレイヤーのHPが0になった GAMEOVER
CLEAR 戻るボタンを押した TITLE
GAMEOVER 戻るボタンを押した TITLE

練習4 状態遷移を答えよう

空欄に状態名を書こう。

(1)タイトル画面で開始ボタンを押した

TITLE → ______

(2)プレイ中にゴールした

PLAY → ______

(3)プレイ中にHPが0になった

PLAY → ______

(4)ゲームオーバー画面で戻るボタンを押した

GAMEOVER → ______
練習4の模範解答を見る
(1)TITLE    → PLAY
(2)PLAY     → CLEAR
(3)PLAY     → GAMEOVER
(4)GAMEOVER → TITLE

3.状態に応じてUpdate()を切り替える

3-1.Update()の役割

タイトル画面とプレイ画面では、更新する内容が異なります。

Update()では state の値を調べ、現在の状態に対応する更新関数を呼び出します。

void Update()
{
    Input();

    if (state == TITLE)
    {
        TitleUpdate();
    }
    else if (state == PLAY)
    {
        PlayUpdate();
    }
    else if (state == CLEAR)
    {
        ClearUpdate();
    }
    else if (state == GAMEOVER)
    {
        GameOverUpdate();
    }
}

1フレームで呼び出す状態別の更新関数は、現在の状態に対応する一つだけです。

3-2.状態を変更する場所

状態を変更する条件は、それぞれの更新関数に書きます。

void TitleUpdate()
{
    if (startPressed == true)
    {
        state = PLAY;
    }
}
void PlayUpdate()
{
    if (playerHp <= 0)
    {
        state = GAMEOVER;
    }
    else if (reachedGoal == true)
    {
        state = CLEAR;
    }
}

練習5 Update()を完成させよう

空欄に条件式または関数名を書こう。

void Update()
{
    Input();

    if (____________)
    {
        TitleUpdate();
    }
    else if (state == PLAY)
    {
        ____________;
    }
    else if (state == CLEAR)
    {
        ____________;
    }
    else if (____________)
    {
        GameOverUpdate();
    }
}
練習5の模範解答を見る
void Update()
{
    Input();

    if (state == TITLE)
    {
        TitleUpdate();
    }
    else if (state == PLAY)
    {
        PlayUpdate();
    }
    else if (state == CLEAR)
    {
        ClearUpdate();
    }
    else if (state == GAMEOVER)
    {
        GameOverUpdate();
    }
}

4.状態に応じてDraw()を切り替える

4-1.Update()とDraw()の対応

Draw()でも同じ state を調べ、現在の状態に対応する描画関数を呼び出します。

state Update()で呼ぶ関数 Draw()で呼ぶ関数
TITLE TitleUpdate() TitleDraw()
PLAY PlayUpdate() PlayDraw()
CLEAR ClearUpdate() ClearDraw()
GAMEOVER GameOverUpdate() GameOverDraw()
void Draw()
{
    if (state == TITLE)
    {
        TitleDraw();
    }
    else if (state == PLAY)
    {
        PlayDraw();
    }
    else if (state == CLEAR)
    {
        ClearDraw();
    }
    else if (state == GAMEOVER)
    {
        GameOverDraw();
    }
}

Update()は値や状態を変える処理、Draw()は現在の内容を画面に表示する処理です。


練習6 Draw()を完成させよう

Update()との対応を考えて、空欄を埋めよう。

void Draw()
{
    if (state == TITLE)
    {
        ____________;
    }
    else if (state == ______)
    {
        PlayDraw();
    }
    else if (state == CLEAR)
    {
        ____________;
    }
    else if (state == GAMEOVER)
    {
        ____________;
    }
}
練習6の模範解答を見る
void Draw()
{
    if (state == TITLE)
    {
        TitleDraw();
    }
    else if (state == PLAY)
    {
        PlayDraw();
    }
    else if (state == CLEAR)
    {
        ClearDraw();
    }
    else if (state == GAMEOVER)
    {
        GameOverDraw();
    }
}

練習7 1フレームずつ処理をトレースしよう

ゲームは TITLE の状態から始まります。Update()の中で状態が変わった場合、その直後の Draw()は変更後の状態に対応する関数を呼び出します。

次の出来事が起きたとき、空欄を埋めよう。

フレーム Update前のstate 出来事 呼ばれる更新関数 Update後のstate 呼ばれる描画関数
1 TITLE 何も押さない ______ ____ ______
2 TITLE 開始ボタンを押す ______ ____ ______
3 PLAY ゴールする ______ ____ ______
4 CLEAR 戻るボタンを押す ______ ____ ______
練習7の模範解答を見る
フレーム Update前のstate 出来事 呼ばれる更新関数 Update後のstate 呼ばれる描画関数
1 TITLE 何も押さない TitleUpdate() TITLE TitleDraw()
2 TITLE 開始ボタンを押す TitleUpdate() PLAY PlayDraw()
3 PLAY ゴールする PlayUpdate() CLEAR ClearDraw()
4 CLEAR 戻るボタンを押す ClearUpdate() TITLE TitleDraw()

たとえば2フレーム目では、TitleUpdate()の中で state が PLAY に変わります。その後で Draw()を呼ぶため、同じフレームの描画は PlayDraw()になります。


5.フレームとFPS

5-1.フレームとは

ゲームループの Update()とDraw()を1回実行し、画面を1回更新する単位をフレームといいます。

1フレーム目:Update() → Draw()
2フレーム目:Update() → Draw()
3フレーム目:Update() → Draw()
          …

5-2.FPSとは

**FPS(Frames Per Second)**は、1秒間に処理・表示するフレーム数です。

60 FPS = 1秒間に60フレーム
30 FPS = 1秒間に30フレーム

1フレームにかかる時間は、次の式で求められます。

$$ 1フレームの時間(秒)= \frac{1}{FPS} $$

FPS 1フレームの時間 ミリ秒に直した値
30 FPS 約0.0333秒 約33.3 ms
60 FPS 約0.0167秒 約16.7 ms
120 FPS 約0.0083秒 約8.3 ms

練習8 フレームとFPSを計算しよう

(1)60 FPSで3秒間動かした

処理するフレーム数:______フレーム

(2)30 FPSの1フレーム

1フレームの時間:1 ÷ 30 = 約______秒

(3)120 FPSで2秒間動かした

処理するフレーム数:______フレーム

練習8の模範解答を見る
(1)60 × 3 = 180フレーム
(2)1 ÷ 30 = 約0.0333秒
(3)120 × 2 = 240フレーム

6.デルタタイム

6-1.デルタタイムとは

**デルタタイム(deltaTime)**は、前のフレームから現在のフレームまでに経過した時間です。通常は秒単位で扱います。

60 FPSで安定している場合、デルタタイムはおよそ次の値になります。

deltaTime = 1 ÷ 60 = 約0.0167秒

実際のゲームでは、毎フレームの処理時間が少しずつ変わるため、deltaTimeも一定とは限りません。

FPS・フレーム・デルタタイムの関係FPS・フレーム・デルタタイムの関係

図3 FPS・フレーム・デルタタイムの関係

6-2.速さから1フレームの移動量を求める

1秒当たりの速さが決まっているとき、1フレームの移動量は次の式で求めます。

$$ 移動量 = 速さ \times deltaTime $$

たとえば、1秒間に180ピクセル移動するプレイヤーを60 FPSで動かします。

1フレームの移動量
= 180 × 1/60
= 3ピクセル

座標の更新は、次のように書けます。

playerX ← playerX + speed × deltaTime

6-3.なぜデルタタイムを掛けるのか

毎フレーム4ピクセルずつ移動させると、FPSによって1秒後の移動距離が変わります。

FPS 1フレームの移動量 1秒間の移動距離
30 FPS 4ピクセル 120ピクセル
60 FPS 4ピクセル 240ピクセル

speed × deltaTimeで移動量を求めれば、FPSが変わっても1秒当たりの移動距離をそろえられます。


練習9 デルタタイムで移動量を求めよう

プレイヤーの速さを毎秒120ピクセルとします。

(1)deltaTimeが0.02秒だった

移動量 = 120 × 0.02 = ______ピクセル

(2)deltaTimeが0.01秒だった

移動量 = 120 × 0.01 = ______ピクセル

(3)2フレーム分の移動距離

(1)と(2)の2フレームで、合計何ピクセル移動するか答えよう。

合計 = ______ピクセル
練習9の模範解答を見る
(1)120 × 0.02 = 2.4ピクセル
(2)120 × 0.01 = 1.2ピクセル
(3)2.4 + 1.2 = 3.6ピクセル

練習10 座標の変化をトレースしよう

最初の座標を playerX = 20.0、速さを毎秒100ピクセルとします。

playerX ← playerX + 100 × deltaTime

各フレームの deltaTimeを使い、playerXの変化を追跡しよう。

フレーム deltaTime 移動量 更新後のplayerX
最初 ― ― 20.0
1 0.016 ____ ____
2 0.020 ____ ____
3 0.014 ____ ____
練習10の模範解答を見る
フレーム deltaTime 移動量 更新後のplayerX
最初 ― ― 20.0
1 0.016 1.6 21.6
2 0.020 2.0 23.6
3 0.014 1.4 25.0

3フレームの経過時間は 0.016 + 0.020 + 0.014 = 0.050秒です。移動距離は 100 × 0.050 = 5.0ピクセルになります。


7.総合問題

問題1 ミニゲームのUpdate()とDraw()を完成させよう

次のゲームには、TITLE、PLAY、GAMEOVERの3つの状態があります。空欄を埋めよう。

void Update()
{
    ________();

    if (state == TITLE)
    {
        ____________;
    }
    else if (state == PLAY)
    {
        ____________;
    }
    else if (state == GAMEOVER)
    {
        ____________;
    }
}

void Draw()
{
    if (state == TITLE)
    {
        ____________;
    }
    else if (state == PLAY)
    {
        ____________;
    }
    else if (state == GAMEOVER)
    {
        ____________;
    }
}
問題1の模範解答を見る
void Update()
{
    Input();

    if (state == TITLE)
    {
        TitleUpdate();
    }
    else if (state == PLAY)
    {
        PlayUpdate();
    }
    else if (state == GAMEOVER)
    {
        GameOverUpdate();
    }
}

void Draw()
{
    if (state == TITLE)
    {
        TitleDraw();
    }
    else if (state == PLAY)
    {
        PlayDraw();
    }
    else if (state == GAMEOVER)
    {
        GameOverDraw();
    }
}

問題2 1秒後の状態を求めよう

プレイヤーの最初の座標は playerX = 50.0、速さは毎秒180ピクセルです。右方向へ1秒間移動しました。

(1)60 FPSで、deltaTimeを使った

playerX ← playerX + 180 × deltaTime

1秒後の playerX:______

(2)30 FPSで、毎フレーム3ピクセル移動した

playerX ← playerX + 3

1秒後の playerX:______

(3)結果の違いを説明しよう

________________________________

問題2の模範解答を見る

(1)

50.0 + 180 × 1.0 = 230.0

(2)

50.0 + 3 × 30 = 140.0

(3)

(1)は、1秒当たりの速さに経過時間を掛けているため、1秒で180ピクセル移動します。

(2)は1フレーム当たりの移動量を固定しているため、FPSによって1秒間の移動距離が変わります。


8.まとめ

  • Initialize()はゲーム開始時に1回実行する
  • Update()とDraw()は、ゲームループの中で毎フレーム実行する
  • Input()は Update()の中で毎フレーム呼び出す
  • Release()はゲーム終了時に1回実行する
  • 現在の画面や進行状況は、状態変数 stateで管理する
  • Update()とDraw()は、同じ stateを使って処理を切り替える
  • 状態の変更は更新処理で行い、描画処理は現在の状態を表示する
  • フレームは、ゲームの処理と画面更新を1回行う単位である
  • FPSは、1秒間に処理・表示するフレーム数である
  • デルタタイムは、前のフレームから現在のフレームまでの経過時間である
  • 1フレームの移動量は、速さ × deltaTimeで求める