Appearance
第10回 タイトル画面と状態遷移の整理
前回: 第09回 敵AIとステートマシン / 次回: 第11回 操作感の設計と改善
今回の授業内容
- 第02回から使っている
GameStateへ、タイトル状態を追加する。 - 敵1体の
EnemyStateと、ゲーム全体のGameStateの範囲を区別する。 - 状態の直接代入を
ChangeGameState()へ集約する。 - タイトル、プレイ中、ゲームクリア、ゲームオーバーを行き来する。
- 状態が変わった瞬間に、BGM、SE、リセット処理を実行する。
今回の配布コード
第09回までのプレイ中、ゲームクリア、ゲームオーバーへ、タイトル状態を追加したコードです。
第02回で学んだゲーム状態を作り直すのではなく、状態変更が増えてもBGMや初期化を忘れない形へ整理します。
配布ファイル一覧
| ファイル | 内容 |
|---|---|
| Source/Game.h | GameStateと遷移関数の宣言 |
| Source/Game.cpp | 状態別更新、遷移、リセット |
| Source/SoundManager.h | タイトルBGMの音声IDを追加 |
| Source/SoundManager.cpp | 状態遷移から利用する音響管理 |
| Assets/Sounds/TitleBgm.wav | タイトル画面で再生するBGM |
第02回のGameStateへTitleを追加する
第02回では、Playing、GameClear、GameOverを使って更新処理を切り替えました。
今回は、その先頭へTitleを追加します。
Game.hcpp
enum GameState
{
Title,
Playing,
GameClear,
GameOver
};ゲーム起動直後の初期状態もTitleへ変更します。
Game.hcpp
GameState gameState = Title;タイトル画面では、プレイヤーや敵をまだ更新しません。
SPACEを押したときだけStartPlay()を呼び、ゲームを開始します。
EnemyStateとGameStateを区別する
| 状態 | 所有する単位 | 制御するもの |
|---|---|---|
EnemyState | 敵1体 | 待機、巡回、追跡、やられ |
PlayerAnimation | プレイヤー1体 | アニメーション表示 |
GameState | ゲーム全体 | 画面、入力、全キャラクターの更新 |
敵がIdleでも、ゲーム全体はPlayingのままです。ゲームがGameOverになると、すべての敵がどの状態であっても更新されません。
それぞれの状態を一から書き直す必要はありません。
どのデータが、どこまでの処理を切り替えているかを確認します。
ゲーム全体の状態遷移を確認する
図を準備しています…
状態ごとに更新処理を選ぶ
Game.cppcpp
switch (gameState)
{
case Title:
UpdateTitle();
break;
case Playing:
UpdatePlaying();
break;
case GameClear:
case GameOver:
UpdateResult();
break;
}Playing以外ではUpdatePlaying()を呼ばないため、プレイヤーと全敵の更新が止まります。各処理の中で何度もgameStateを確認するのではなく、更新処理の入口で実行対象を選んでいます。
状態変更時の処理を集約する
以前は、状態を変更する場所ごとにBGM停止やSE再生を書いていました。必要な処理を忘れないように、配布コードではChangeGameState()へ集約しています。
Game.cppcpp
void Game::ChangeGameState(GameState nextState)
{
gameState = nextState;
switch (gameState)
{
case Title:
sounds.PlayBgm(SoundId::TitleBgm);
break;
case Playing:
sounds.PlayBgm(SoundId::PlayingBgm);
break;
case GameClear:
sounds.StopBgm();
sounds.PlaySe(SoundId::Goal);
break;
case GameOver:
sounds.StopBgm();
sounds.PlaySe(SoundId::GameOver);
break;
}
}状態を変更する側は、次の状態だけを指定します。
cpp
ChangeGameState(GameOver);プレイ開始前に全データを戻す
Game.cppcpp
void Game::StartPlay()
{
ResetPlay();
ChangeGameState(Playing);
}
void Game::ResetPlay()
{
ResetPlayer();
ResetEnemies();
cameraX = 0;
}リトライとタイトルからの開始が同じStartPlay()を使うため、初期化項目が分岐しません。敵はCSVの配置から全個体を復元し、状態もIdleへ戻ります。
終了条件の優先順位を決める
同じフレームで落下、敵接触、ゴール接触が成立する可能性があります。状態変更後も処理を続けると、後の判定で結果が上書きされます。
今回の配布コードでは、次の順序で判定します。
- プレイヤーの落下。
- 敵との接触。
- ゴールとの接触。
cpp
if (playerFell)
{
ChangeGameState(GameOver);
return;
}終了条件が成立したらreturnし、そのフレームの残りの判定を行いません。
落下、敵接触、ゴールが同時に成立しそうな場所でも、最初に成立した結果が後の判定で上書きされなくなります。
状態遷移を実際にたどる
コードを入力したら、次の順番で4つの状態を行き来します。
- ゲームを起動し、タイトル画面とタイトルBGMを確認する。
- SPACEを押し、プレイ画面とプレイ中BGMへ切り替わることを確認する。
- 敵へ横から触れ、ゲームオーバーSEが1回だけ鳴ることを確認する。
- Rを押し、プレイヤーとすべての敵が初期位置へ戻ることを確認する。
- Tを押し、タイトル画面とタイトルBGMへ戻ることを確認する。
- ゴールへ到達し、ゲームクリアSEと結果表示を確認する。
画面だけでなく、音が切り替わる瞬間にも注目します。
BGMを毎フレーム再生するのではなく、ChangeGameState()が呼ばれた瞬間にだけ切り替えています。
タイトル画面をカスタマイズする
タイトル画面の文字はDrawTitle()へまとまっています。
Game.cppcpp
void Game::DrawTitle() const
{
DrawString(
150, 150,
L"2D PLATFORMER SAMPLE",
GetColor(20, 20, 30));
DrawString(
125, 220,
L"PRESS SPACE TO START",
GetColor(20, 20, 30));
}文字列、座標、色のうち1つを変更し、自分のタイトル画面にします。
日本語を表示する場合は、文字列の先頭にLを付けたワイド文字列を使用します。
次の変更から1つを選べます。
- ゲーム名を自分で決める。
- タイトル文字の位置を変える。
GetColor()のRGB値を変える。- 操作説明を追加する。
- SPACE以外の開始キーへ変更する。
開始キーを変更した場合は、画面に表示する操作説明も同じキーへ変更します。
今回の完成条件
- 第09回の敵AIと物理処理が残っている。
- 起動直後にタイトル画面が表示される。
- SPACEでプレイを開始できる。
- クリア、ゲームオーバー、リトライ、タイトル復帰ができる。
- 状態変更に合わせてBGMとSEが切り替わる。
- リトライ時に、プレイヤー、敵、カメラが初期状態へ戻る。
- タイトルの文字列、位置、色、操作のいずれかを変更できる。
よくある問題
タイトル画面でもプレイヤーや敵が動く
Update()のTitleでUpdateTitle()だけを呼んでいるか確認します。UpdatePlaying()はPlayingの場合だけ呼びます。
リトライ後に敵が消えたままになる
StartPlay()からResetPlay()を呼び、その中でResetEnemies()を実行しているか確認します。
BGMが重なって聞こえる
状態を変更する場所で直接PlayBgm()を呼ばず、ChangeGameState()へまとめます。SoundManager::PlayBgm()にも、同じBGMを重複再生しない処理が残っていることを確認します。
タイトルへ戻ってもプレイ中BGMが鳴る
Tを押したとき、gameState = TitleではなくChangeGameState(Title)を呼びます。
まとめ
- 第02回から使っている
GameStateへTitleを追加しました。 - 敵1体の状態と、ゲーム全体の状態の範囲を区別しました。
- BGM、SE、画面切り替えに伴う処理を
ChangeGameState()へまとめました。 StartPlay()からプレイヤー、敵、カメラを同じ手順で初期化しました。- タイトル、プレイ、クリア、ゲームオーバーを実際に行き来しました。
- タイトル画面の文字列、位置、色、操作を変更できるようになりました。