ゲームの UWP アプリ フレームワークの定義

注意

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

ユニバーサル Windows プラットフォーム (UWP) ゲームのコーディングでの最初の手順は、アプリ オブジェクトが Windows と対話できるフレームワークを構築することです。これには、中断/再開イベントの処理、ウィンドウの表示の変更、スナップなどの Windows ランタイム機能が含まれます。

目標

  • ユニバーサル Windows プラットフォーム (UWP) DirectX ゲーム用のフレームワークをセットアップし、ゲーム全体のフローを定義するステート マシンを実装します。

注意

このトピックに従って、ダウンロードした Simple3DGameDX サンプル ゲームのソース コードを確認します。

はじめに

ゲーム プロジェクトのセットアップ」トピックでは、wWinMain 関数と、IFrameworkViewSource および IFrameworkView インターフェイスについて説明しました。 App クラス (Simple3DGameDX プロジェクトの App.cpp ソース コード ファイルで定義されていることを確認できます) は、"ビュー プロバイダー ファクトリー""ビュー プロバイダー" の両方として機能することを学習しました。

このトピックではそこから説明し、ゲーム内の App クラスで IFrameworkView のメソッドを実装する方法について詳しく説明します。

App::Initialize メソッド

アプリケーションの起動時に、Windows によって最初に呼び出されるメソッドは IFrameworkView::Initialize の実装です。

この実装では、UWP ゲームの最も基本的な動作を処理する必要があります。たとえば、中断 (およびその後の再開) イベントにサブスクライブすることで、これらのイベントを確実に処理できるようにします。 ここではディスプレイ アダプター デバイスにもアクセスできます。そのため、デバイスに依存するグラフィックス リソースを作成できます。

void Initialize(CoreApplicationView const& applicationView)
{
    applicationView.Activated({ this, &App::OnActivated });

    CoreApplication::Suspending({ this, &App::OnSuspending });

    CoreApplication::Resuming({ this, &App::OnResuming });

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

生ポインターは可能な限り避けます (ほぼ常に可能です)。

  • Windows ランタイム型では、ポインターを完全に回避し、スタックに値を作成するだけで済む場合が非常に多いです。 ポインターが必要な場合は winrt::com_ptr を使用します (その例はこの後すぐに説明します)。
  • 一意のポインターの場合は std::unique_ptrstd::make_unique を使用します。
  • 共有ポインターの場合は std::shared_ptrstd::make_shared を使用します。

App::SetWindow メソッド

Initialize の実行後に、Windows によって IFrameworkView::SetWindow の実装が呼び出され、ゲームのメイン ウィンドウを表す CoreWindow オブジェクトが渡されます。

App::SetWindow で、ウィンドウ関連のイベントをサブスクライブし、ウィンドウと表示の一部の動作を構成します。 たとえば、マウスとタッチの両方のコントロールで使用できるマウス ポインターを構築します (CoreCursor クラスを使用)。 また、ウィンドウ オブジェクトをデバイス依存リソース オブジェクトに渡します。

イベント処理の詳細については、「ゲームのフロー管理」トピックで説明します。

void SetWindow(CoreWindow const& window)
{
    //CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();

    window.PointerCursor(CoreCursor(CoreCursorType::Arrow, 0));

    PointerVisualizationSettings visualizationSettings{ PointerVisualizationSettings::GetForCurrentView() };
    visualizationSettings.IsContactFeedbackEnabled(false);
    visualizationSettings.IsBarrelButtonFeedbackEnabled(false);

    m_deviceResources->SetWindow(window);

    window.Activated({ this, &App::OnWindowActivationChanged });

    window.SizeChanged({ this, &App::OnWindowSizeChanged });

    window.Closed({ this, &App::OnWindowClosed });

    window.VisibilityChanged({ this, &App::OnVisibilityChanged });

    DisplayInformation currentDisplayInformation{ DisplayInformation::GetForCurrentView() };

    currentDisplayInformation.DpiChanged({ this, &App::OnDpiChanged });

    currentDisplayInformation.OrientationChanged({ this, &App::OnOrientationChanged });

    currentDisplayInformation.StereoEnabledChanged({ this, &App::OnStereoEnabledChanged });

    DisplayInformation::DisplayContentsInvalidated({ this, &App::OnDisplayContentsInvalidated });
}

App::Load メソッド

メイン ウィンドウを設定したので、IFrameworkView::Load の実装が呼び出されます。 ゲームのデータやアセットをプリフェッチするには、InitializeSetWindow よりも、Load が適しています。

void Load(winrt::hstring const& /* entryPoint */)
{
    if (!m_main)
    {
        m_main = winrt::make_self<GameMain>(m_deviceResources);
    }
}

ご覧のように、実際の作業は、ここで作成した GameMain オブジェクトのコンストラクターによって行われます。 GameMain クラスは GameMain.hGameMain.cpp で定義されています。

GameMain::GameMain コンストラクター

GameMain コンストラクター (およびそれが呼び出す他のメンバー関数) は、ゲーム オブジェクトの作成、グラフィックス リソースの読み込み、ゲームのステート マシンの初期化を行う一連の非同期読み込み操作を開始します。 開始時の状態やグローバルな値の設定など、ゲームを開始する前に必要な準備も行います。

Windows では、ゲームによって入力の処理が開始されるまでにかかる時間が制限されます。 そのため、このように非同期を使用すると、Load では、開始した作業がバックグラウンドで続行されている間でもすぐに戻ることができることを意味します。 リソースが多いなど、読み込みに時間がかかる場合は、頻繁に更新される進行状況バーを用意することをお勧めします。

非同期プログラミングに慣れていない場合は、「C++/WinRT を使用した同時実行操作と非同期操作」を参照してください。

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) :
    m_deviceResources(deviceResources),
    m_windowClosed(false),
    m_haveFocus(false),
    m_gameInfoOverlayCommand(GameInfoOverlayCommand::None),
    m_visible(true),
    m_loadingCount(0),
    m_updateState(UpdateEngineState::WaitingForResources)
{
    m_deviceResources->RegisterDeviceNotify(this);

    m_renderer = std::make_shared<GameRenderer>(m_deviceResources);
    m_game = std::make_shared<Simple3DGame>();

    m_uiControl = m_renderer->GameUIControl();

    m_controller = std::make_shared<MoveLookController>(CoreWindow::GetForCurrentThread());

    auto bounds = m_deviceResources->GetLogicalSize();

    m_controller->SetMoveRect(
        XMFLOAT2(0.0f, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(GameUIConstants::TouchRectangleSize, bounds.Height)
        );
    m_controller->SetFireRect(
        XMFLOAT2(bounds.Width - GameUIConstants::TouchRectangleSize, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(bounds.Width, bounds.Height)
        );

    SetGameInfoOverlay(GameInfoOverlayState::Loading);
    m_uiControl->SetAction(GameInfoOverlayCommand::None);
    m_uiControl->ShowGameInfoOverlay();

    // Asynchronously initialize the game class and load the renderer device resources.
    // By doing all this asynchronously, the game gets to its main loop more quickly
    // and in parallel all the necessary resources are loaded on other threads.
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    auto lifetime = get_strong();

    m_game->Initialize(m_controller, m_renderer);

    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);

    // The finalize code needs to run in the same thread context
    // as the m_renderer object was created because the D3D device context
    // can ONLY be accessed on a single thread.
    // co_await of an IAsyncAction resumes in the same thread context.
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();

    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        // In the middle of a game so spin up the async task to load the level.
        co_await m_game->LoadLevelAsync();

        // The m_game object may need to deal with D3D device context work so
        // again the finalize code needs to run in the same thread
        // context as the m_renderer object was created because the D3D
        // device context can ONLY be accessed on a single thread.
        m_game->FinalizeLoadLevel();
        m_game->SetCurrentLevelToSavedState();
        m_updateState = UpdateEngineState::ResourcesLoaded;
    }
    else
    {
        // The game is not in the middle of a level so there aren't any level
        // resources to load.
    }

    // Since Game loading is an async task, the app visual state
    // may be too small or not be activated. Put the state machine
    // into the correct state to reflect these cases.

    if (m_deviceResources->GetLogicalSize().Width < GameUIConstants::MinPlayableWidth)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::TooSmall;
        m_controller->Active(false);
        m_uiControl->HideGameInfoOverlay();
        m_uiControl->ShowTooSmall();
        m_renderNeeded = true;
    }
    else if (!m_haveFocus)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::Deactivated;
        m_controller->Active(false);
        m_uiControl->SetAction(GameInfoOverlayCommand::None);
        m_renderNeeded = true;
    }
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    ...
}

コンストラクターによって開始される一連の作業の概要を次に示します。

  • GameRenderer 型のオブジェクトを作成して初期化します。 詳細については、「レンダリング フレームワーク I: レンダリングの概要」を参照してください。
  • Simple3DGame 型のオブジェクトを作成して初期化します。 詳細については、「メイン ゲーム オブジェクトの定義」を参照してください。
  • ゲームの UI コントロール オブジェクトを作成し、リソース ファイルの読み込み時に進行状況バーを表示するゲーム情報オーバーレイを表示します。 詳細については、「ユーザー インターフェイスの追加」を参照してください。
  • コントローラー (タッチ、マウス、またはゲーム コントローラー) からの入力を読み取るコントローラー オブジェクトを作成します。 詳細については、「コントロールの追加」を参照してください。
  • 移動とカメラのそれぞれのタッチ コントロール用に、画面の左下と右下の 2 つの四角形の領域を定義します。 左下の四角形 (SetMoveRect への呼び出しで定義される) は、カメラを前後左右に動かすための仮想のコントロール パッドとして使われ、 右下の四角形 (SetFireRect メソッドで定義される) は、弾を撃つための仮想のボタンとして使われます。
  • コルーチンを使用して、リソースの読み込みを別々のステージに分割します。 Direct3D のデバイス コンテキストへのアクセスはそのデバイス コンテキストが作成されたスレッドに制限されるのに対し、オブジェクト作成用の Direct3D デバイスへのアクセスはスレッドが制限されません。 そのため、GameRenderer::CreateGameDeviceResourcesAsync コルーチンは、元のスレッドで実行される完了タスク (GameRenderer::FinalizeCreateGameDeviceResources) とは別のスレッドで実行できます。
  • Simple3DGame::LoadLevelAsyncSimple3DGame::FinalizeLoadLevel を使うレベル リソースの読み込みにも同様のパターンを使います。

GameMain::InitializeGameState の詳細は、次のトピック「ゲームのフロー管理」で説明します。

App::OnActivated メソッド

次に、CoreApplicationView::Activated イベントが発生します。 そのため、実装した OnActivated イベント ハンドラー (App::OnActivated メソッドなど) が呼び出されます。

void OnActivated(CoreApplicationView const& /* applicationView */, IActivatedEventArgs const& /* args */)
{
    CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();
}

ここで行う唯一の作業は、メイン CoreWindow をアクティブ化することです。 または、その作業を App::SetWindow で行うこともできます。

App::Run メソッド

InitializeSetWindowLoad でステージを設定しました。 ゲームが起動して実行されているので、IFrameworkView::Run の実装が呼び出されます。

void Run()
{
    m_main->Run();
}

ここでも、作業は GameMain によって行われます。

GameMain::Run メソッド

GameMain::Run はゲームのメイン ループで、GameMain.cpp にあります。 基本的なロジックは、ゲームのウィンドウが開いたままになっている間、すべてのイベントをディスパッチし、タイマーを更新し、グラフィックス パイプラインの結果をレンダリングして表示するというものです。 また、ゲームの状態間の移行に使われるイベントのディスパッチと処理が行われます。

このコードは、ゲーム エンジン ステート マシンの 2 つの状態にも関係しています。

  • UpdateEngineState::Deactivated。 これは、ゲーム ウィンドウが非アクティブ化されている (フォーカスを失っている) か、スナップされていることを指定します。
  • UpdateEngineState::TooSmall。 これは、クライアント領域のサイズが小さすぎてゲームをレンダリングできないことを指定します。

どちらの状態でも、ゲームはイベント処理を中断し、ウィンドウがアクティブになるか、スナップが解除されるか、サイズが変更されるのを待機します。

ゲーム ウィンドウが表示されている間 (Window.Visibletrue)、メッセージ キューに到達する各イベントを処理する必要があるため、ProcessAllIfPresent オプションを指定して CoreWindowDispatch.ProcessEvents を呼び出す必要があります。 他のオプションでは、メッセージ イベントの処理に遅延が発生することがあり、この場合、ゲームが応答しなくなったように見えるか、タッチ動作の反応が遅く感じられる場合があります。

ゲームが表示されて "いない" false(Window.Visible) ときや、中断されている、またはサイズが小さすぎる (スナップ状態) ときにリソースを循環させてどこにも到達しないメッセージをディスパッチすることは回避する必要があります。 この場合、ゲームでは ProcessOneAndAllPending オプションを使用する必要があります。 このオプションでは、イベントが取得されるまではブロックが行われ、その後、そのイベント (およびそのイベントの処理中にプロセス キューに到達した他のイベント) が処理されます。 その後、キューの処理が終了すると、CoreWindowDispatch.ProcessEvents は即座に戻ります。

下に示すコード例では、m_visible データ メンバーはウィンドウの可視性を表します。 ゲームが中断されると、そのウィンドウは表示されません。 ウィンドウが表示されて "いる" 場合、m_updateState の値 (UpdateEngineState 列挙型) によって、ウィンドウが非アクティブ化されている (フォーカスが失われている)、サイズが小さすぎる (スナップ状態)、または適切なサイズであるかどうかがさらに判断されます。

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                if (m_updateStateNext == UpdateEngineState::WaitingForResources)
                {
                    WaitingForResourceLoading();
                    m_renderNeeded = true;
                }
                else if (m_updateStateNext == UpdateEngineState::ResourcesLoaded)
                {
                    // In the device lost case, we transition to the final waiting state
                    // and make sure the display is updated.
                    switch (m_pressResult)
                    {
                    case PressResultState::LoadGame:
                        SetGameInfoOverlay(GameInfoOverlayState::GameStats);
                        break;

                    case PressResultState::PlayLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::LevelStart);
                        break;

                    case PressResultState::ContinueLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::Pause);
                        break;
                    }
                    m_updateStateNext = UpdateEngineState::WaitingForPress;
                    m_uiControl->ShowGameInfoOverlay();
                    m_renderNeeded = true;
                }

                if (!m_renderNeeded)
                {
                    // The App is not currently the active window and not in a transient state so just wait for events.
                    CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
                    break;
                }
                // otherwise fall through and do normal processing to get the rendering handled.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

App::Uninitialize メソッド

ゲームが終了すると IFrameworkView::Uninitialize の実装が呼び出されます。 これはクリーンアップを実行する機会です。 アプリ ウィンドウを閉じてもアプリのプロセスは強制終了されず、代わりに、アプリ シングルトンの状態がメモリに書き込まれます。 システムでこのメモリを再利用する際に、リソースの特別なクリーンアップなどの特別な処理が必要な場合は、そのクリーンアップ用のコードを Uninitialize に入れます。

この例では、App::Uninitialize で処理は行われません。

void Uninitialize()
{
}

ヒント

ゲームを開発するときは、このトピックで説明したメソッドの近くにスタートアップ コードを設計してください。 各メソッドの基本的な推奨事項を次に示します。

  • メイン クラスの割り当てと基本的なイベント ハンドラーの接続には Initialize を使います。
  • ウィンドウ固有のイベントをサブスクライブし、メイン ウィンドウをデバイス依存リソース オブジェクトに渡して、スワップ チェーンの作成時にウィンドウを使用できるようにする場合は、SetWindow を使います。
  • その他のセットアップの処理と非同期のオブジェクト作成やリソース読み込みには Load を使います。 一時ファイルまたは一時データを作成する必要がある場合は (手続き的に生成されるアセットなど)、その処理もこのメソッドで行います。

次の手順

このトピックでは、DirectX を使用する UWP ゲームの基本的な構造について説明しました。 この後のトピックでも取り上げるため、これらのメソッドを念頭に置いておくことをお勧めします。

次のトピック「ゲームのフロー管理」で、ゲームを続行するために、ゲームの状態とイベント処理を管理する方法について詳しく説明します。