第5部 ゲームの基本的な構成
第5部 ゲームの基本的な構成
このワークの目標
ゲームは、入力・更新・描画を何度も繰り返して動いています。
このワークでは、次の内容を確認します。
- ゲームループの処理順を説明できる
Initialize()、Update()、Draw()、Release()の役割を区別できる- タイトル・プレイ・クリア・ゲームオーバーの状態遷移を追跡できる
- 現在の状態に応じて、
Update()とDraw()の処理を切り替えられる - フレーム・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も一定とは限りません。
図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で求める