Skip to content

第10回 タイトル画面と状態遷移の整理

前回: 第09回 敵AIとステートマシン / 次回: 第11回 操作感の設計と改善

今回の授業内容

  • 第02回から使っているGameStateへ、タイトル状態を追加する。
  • 敵1体のEnemyStateと、ゲーム全体のGameStateの範囲を区別する。
  • 状態の直接代入をChangeGameState()へ集約する。
  • タイトル、プレイ中、ゲームクリア、ゲームオーバーを行き来する。
  • 状態が変わった瞬間に、BGM、SE、リセット処理を実行する。

今回の配布コード

第09回までのプレイ中、ゲームクリア、ゲームオーバーへ、タイトル状態を追加したコードです。
第02回で学んだゲーム状態を作り直すのではなく、状態変更が増えてもBGMや初期化を忘れない形へ整理します。

第10回のコード一式をダウンロード

配布ファイル一覧
ファイル内容
Source/Game.hGameStateと遷移関数の宣言
Source/Game.cpp状態別更新、遷移、リセット
Source/SoundManager.hタイトルBGMの音声IDを追加
Source/SoundManager.cpp状態遷移から利用する音響管理
Assets/Sounds/TitleBgm.wavタイトル画面で再生するBGM

第02回のGameStateへTitleを追加する

第02回では、PlayingGameClearGameOverを使って更新処理を切り替えました。
今回は、その先頭へ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へ戻ります。

終了条件の優先順位を決める

同じフレームで落下、敵接触、ゴール接触が成立する可能性があります。状態変更後も処理を続けると、後の判定で結果が上書きされます。

今回の配布コードでは、次の順序で判定します。

  1. プレイヤーの落下。
  2. 敵との接触。
  3. ゴールとの接触。
cpp
if (playerFell)
{
    ChangeGameState(GameOver);
    return;
}

終了条件が成立したらreturnし、そのフレームの残りの判定を行いません。
落下、敵接触、ゴールが同時に成立しそうな場所でも、最初に成立した結果が後の判定で上書きされなくなります。

状態遷移を実際にたどる

コードを入力したら、次の順番で4つの状態を行き来します。

  1. ゲームを起動し、タイトル画面とタイトルBGMを確認する。
  2. SPACEを押し、プレイ画面とプレイ中BGMへ切り替わることを確認する。
  3. 敵へ横から触れ、ゲームオーバーSEが1回だけ鳴ることを確認する。
  4. Rを押し、プレイヤーとすべての敵が初期位置へ戻ることを確認する。
  5. Tを押し、タイトル画面とタイトルBGMへ戻ることを確認する。
  6. ゴールへ到達し、ゲームクリア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()TitleUpdateTitle()だけを呼んでいるか確認します。
UpdatePlaying()Playingの場合だけ呼びます。

リトライ後に敵が消えたままになる

StartPlay()からResetPlay()を呼び、その中でResetEnemies()を実行しているか確認します。

BGMが重なって聞こえる

状態を変更する場所で直接PlayBgm()を呼ばず、ChangeGameState()へまとめます。
SoundManager::PlayBgm()にも、同じBGMを重複再生しない処理が残っていることを確認します。

タイトルへ戻ってもプレイ中BGMが鳴る

Tを押したとき、gameState = TitleではなくChangeGameState(Title)を呼びます。

まとめ

  • 第02回から使っているGameStateTitleを追加しました。
  • 敵1体の状態と、ゲーム全体の状態の範囲を区別しました。
  • BGM、SE、画面切り替えに伴う処理をChangeGameState()へまとめました。
  • StartPlay()からプレイヤー、敵、カメラを同じ手順で初期化しました。
  • タイトル、プレイ、クリア、ゲームオーバーを実際に行き来しました。
  • タイトル画面の文字列、位置、色、操作を変更できるようになりました。