ゲームのフロー管理

注意

このトピックは、「DirectX を使った単純なユニバーサル Windows プラットフォーム (UWP) ゲームの作成」チュートリアル シリーズの一部です。 リンク先のトピックでは、このシリーズのコンテキストを設定しています。

これで、ゲームにウィンドウが表示され、いくつかのイベント ハンドラーが登録され、アセットが非同期で読み込まれました。 このトピックでは、ゲームの状態の使用方法、特定の主要なゲームの状態を管理する方法、ゲーム エンジンの更新ループを作成する方法について説明します。 次に、ユーザー インターフェイスのフローについて学習し、最後に、UWP ゲームに必要なイベント ハンドラーについての理解を深めます。

ゲームのフローを管理するために使用するゲームの状態

ゲームのフローを管理するには、ゲームの状態を使用します。

Simple3DGameDX サンプル ゲームをマシンで初めて実行すると、ゲームが開始されていない状態になります。 その後、ゲームを実行すると、次のいずれかの状態になります。

  • ゲームが開始されていないか、ゲームが 2 つのレベルの中間にある状態 (ハイ スコアは 0)。
  • ゲーム ループが実行中で、いずれかのレベル中の状態。
  • ゲームが完了し、ゲーム ループが実行されていない状態 (ハイ スコアは 0 以外の値)。

ゲームは、必要なだけいくつでも状態を保有することができます。 ただし、いつでも終了できることに注意してください。 また、再開するとき、ユーザーは終了時の状態で再開することを想定します。

ゲームの状態管理

ゲームの初期化中に、ゲームのコールド スタートをサポートし、フライトを停止した後にゲームを再開する必要があります。 Simple3DGameDX サンプルでは、停止しないという印象を与えるために、常にゲームの状態が保存されます。

中断イベントに応答して、ゲームプレイは中断されますが、ゲームのリソースはまだメモリ内にあります。 同様に、再開イベントは、サンプル ゲームが中断または終了したときの状態で確実に取得されるように処理されます。 状態に応じて、異なるオプションがプレーヤーに表示されます。

  • ゲームが途中で再開された場合は、一時停止され、オーバーレイで続行オプションが表示されます。
  • また、ゲームが完了した状態から再開された場合は、ハイ スコアと新しいゲームをプレイするオプションが表示されます。
  • そして、いずれかのレベルが開始される前にゲームが再開された場合は、オーバーレイで開始オプションがユーザーに表示されます。

このサンプル ゲームでは、ゲームのコールド スタート、中断イベントがない状態での初めての起動、中断状態からの再開を区別していません。 これは、どの UWP アプリにも適切な設計です。

このサンプルでは、GameMain::InitializeGameState でゲームの状態の初期化が行われます (そのメソッドの概要については次のセクションで説明します)。

フローを視覚化するためのフローチャートを次に示します。 初期化と更新ループの両方を対象としています。

  • 初期化は、Start ノードから始まり、現在のゲームの状態をチェックします。 ゲームのコードについては、次のセクションの GameMain::InitializeGameState の説明をご覧ください。

ゲームのメイン ステート マシン

GameMain::InitializeGameState メソッド

GameMain::InitializeGameState メソッドは、GameMain クラスのコンストラクターを介して間接的に呼び出されます。これは、App::LoadGameMain インスタンスを作成した結果です。

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

ゲーム エンジンを更新する

App::Run メソッドで、GameMain::Run を呼び出します。 GameMain::Run は、ユーザーが実行できる主要なアクションをすべて処理するための基本的なステート マシンです。 このステート マシンの最上位レベルは、ゲームの読み込み、特定レベルのプレイ、ゲームが (システムあるいはユーザーによって) 一時停止された後のレベルの続行を処理します。

サンプル ゲームでは、3 つの主要な状態 (UpdateEngineState 列挙型によって表されます) がゲームに使用できます。

  1. UpdateEngineState::WaitingForResources ゲーム ループが循環していて、リソース (具体的にはグラフィックス リソース) が使用可能になるまで、移行はできません。 非同期のリソース読み込みタスクが完了したら、状態が UpdateEngineState::ResourcesLoaded に更新されます。 これは通常、2 つのレベルの中間で発生します。レベルの中間では、新しいリソースをディスク、ゲーム サーバー、クラウド バックエンドから読み込みます。 このサンプル ゲームでは、その時点ではレベルごとに追加のリソースは必要ないため、この動作はシミュレートされます。
  2. UpdateEngineState::WaitingForPress ゲーム ループが循環していて、特定のユーザー入力を待機しています。 この入力は、プレイヤーによるゲームの読み込み、レベルの開始、またはレベルの続行の操作です。 サンプル コードでは、PressResultState 列挙型を使用してこれらのサブステートを参照しています。
  3. UpdateEngineState::Dynamics ゲーム ループが実行中で、ユーザーがゲームをしています。 ユーザーがプレイ中に、ゲームは移行できる 3 つの条件をチェックします。
  • GameState::TimeExpired レベルの制限時間の有効期限。
  • GameState::LevelComplete プレイヤーによるレベルの完了。
  • GameState::GameComplete プレイヤーによるすべてのレベルの完了。

ゲームは、複数の小さいステート マシンが含まれているステート マシンにすぎません。 それぞれの状態は、非常に具体的な条件によって定義されている必要があります。 ある状態から別の状態への移行は、ユーザー入力またはシステムによる個々の操作 (グラフィックス リソースの読み込みなど) に基づいて行われる必要があります。

ゲームの計画時に、ユーザーやシステムが実行すると考えられるすべての操作に確実に対応できるように、ゲーム フローの全体像を詳細に計画することを検討してください。 ゲームは非常に複雑な場合があり、ステート マシンは、この複雑さを視覚化して扱いやすくする強力なツールです。

更新ループのコードを見てみましょう。

GameMain::Update メソッド

これが、ゲーム エンジンの更新に使われるステート マシンの構造です。

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

ユーザー インターフェイスを更新する

プレイヤーには、システムの状態を継続的に通知して、プレイヤーの操作とゲームを定義するルールに応じて、ゲームの状態を変更できるようにする必要があります。 このサンプル ゲームを含む多くのゲームは、通常、ユーザー インターフェイス (UI) を使用して、プレイヤーにこの情報を表示します。 UI には、ゲームの状態や、スコア、弾薬、残りのチャンスの数などのプレイ固有の情報が表されます。 UI はオーバーレイとも呼ばれます。メインのグラフィックス パイプラインとは別にレンダリングされ、3D プロジェクションの上に配置されるためです。

一部の UI の情報は、ヘッドアップ ディスプレイ (HUD) としても表示され、ユーザーはゲームプレイのメイン領域から完全に視線を移動させなくても、これらの情報を確認できます。 このサンプル ゲームでは、このオーバーレイを Direct2D API を使って作成しています。 または、XAML を使ってこのオーバーレイを作成することもできます。これについては、「サンプル ゲームの拡張」で説明します。

ユーザー インターフェイスには次の 2 つのコンポーネントがあります。

  • スコアとゲームプレイの現在の状態に関する情報が含まれている HUD。
  • 一時停止ビットマップ。これは、ゲームの一時停止/中断状態中にテキストがオーバーレイされる黒の四角形です。 これがゲーム オーバーレイです。 これについては、「ユーザー インターフェイスの追加」で詳しく説明します。

当然のことながら、オーバーレイにもステート マシンがあります。 オーバーレイは、レベル開始またはゲームオーバーのメッセージを表示できます。 これは、ゲームが一時停止または中断されたときに、プレイヤーに表示する必要があるゲームの状態に関する情報を出力できるキャンバスのように機能します。

オーバーレイのレンダリングには、ゲームの状態に応じて、これら 6 つの画面のいずれかになります。

  1. ゲーム開始時のリソース読み込みの進行状況画面。
  2. ゲームプレイの統計情報画面。
  3. レベルの開始メッセージ画面。
  4. 時間切れになる前にすべてのレベルが完了した場合のゲームオーバー画面。
  5. 時間切れになった場合のゲームオーバー画面。
  6. 一時停止メニュー画面。

ユーザー インターフェイスをゲームのグラフィックス パイプラインから分離すると、ゲームのグラフィックス レンダリング エンジンとは別に操作でき、ゲームのコードの複雑さが大幅に軽減されます。

このサンプル ゲームでオーバーレイのステート マシンを構成する方法は次のとおりです。

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

イベント処理

ゲームの UWP アプリ フレームワークの定義」で説明したように、App クラスのビュープロバイダー メソッドの多くはイベント ハンドラーを登録します。 これらのメソッドでは、ゲームのしくみを追加したり、グラフィックス開発を始めたりする前に、これらの重要なイベントを正しく処理する必要があります。

問題のイベントを適切に処理することは、UWP アプリのエクスペリエンスの基礎となります。 UWP アプリはいつでもアクティブ化、非アクティブ化、サイズ変更、スナップ、スナップの解除、中断、再開ができるため、ゲームではこれらのイベント自体をできる限り早く登録し、プレイヤーのエクスペリエンスをスムーズで予測可能な状態に保てる方法で、これらのイベントを処理する必要があります。

次に、このサンプルで使用されているイベント ハンドラーと、ハンドラーが処理するイベントを示します。

イベント ハンドラー 説明
OnActivated CoreApplicationView::Activated を処理します。 ゲーム アプリがフォアグラウンドに表示されているため、メイン ウィンドウがアクティブ化されます。
OnDpiChanged Graphics::Display::DisplayInformation::DpiChanged を処理します。 ディスプレイの DPI が変更されていて、それに応じてゲームそのリソースを調整します。
[CoreWindow] 座標は、Direct2D のデバイスに依存しないピクセル (DIPs) です。 このため、2D アセットまたはプリミティブを正しく表示するには、Direct2D に DPI の変更を通知する必要があります。
OnOrientationChanged Graphics::Display::DisplayInformation::OrientationChanged を処理します。 ディスプレイの向きが変更され、レンダリングを更新する必要があります。
OnDisplayContentsInvalidated Graphics::Display::DisplayInformation::DisplayContentsInvalidated を処理します。 ディスプレイを再描画する必要があり、ゲームをもう一度レンダリングする必要があります。
OnResuming CoreApplication::Resuming を処理します。 ゲーム アプリがゲームを中断状態から復元します。
OnSuspending CoreApplication::Suspending を処理します。 ゲーム アプリがその状態をディスクに保存します。 ストレージへの状態の保存に使用できる時間は 5 秒です。
OnVisibilityChanged CoreWindow::VisibilityChanged を処理します。 ゲーム アプリの表示が切り替わり、表示されるようになったか、別のアプリが表示されたために非表示になったことを示します。
OnWindowActivationChanged CoreWindow::Activated を処理します。 ゲーム アプリのメイン ウィンドウが非アクティブ化またはアクティブ化されたため、フォーカスを動かしてゲームを一時停止するか、フォーカスを再取得する必要があります。 どちらの場合も、ゲームが一時停止されていることがオーバーレイに表示されます。
OnWindowClosed CoreWindow::Closed を処理します。 ゲーム アプリがメイン ウィンドウを閉じ、ゲームを中断します。
OnWindowSizeChanged CoreWindow::SizeChanged を処理します。 サイズ変更に応じてゲーム アプリがグラフィックス リソースとオーバーレイを再割り当てし、その後、レンダー ターゲットを更新します。

次の手順

このトピックでは、ゲームの状態を使用してゲーム フロー全体を管理する方法と、ゲームが複数の異なるステート マシンで構成されていることを説明しました。 また、UI を更新する方法や、主要なアプリのイベント ハンドラーを管理する方法についても説明しました。 これで、レンダリング ループ、ゲーム、そのしくみについて参照する準備が整いました。

このゲームに関して解説した残りのトピックを任意の順序で参照できます。