ゲームの UWP アプリ フレームワークの定義Define the game's UWP app framework

注意

このトピックは、「DirectX チュートリアルシリーズ を含む simple ユニバーサル Windows プラットフォーム (UWP) ゲームの作成 」に含まれています。This topic is part of the Create a simple Universal Windows Platform (UWP) game with DirectX tutorial series. このリンクのトピックでは、系列のコンテキストを設定します。The topic at that link sets the context for the series.

ユニバーサル Windows プラットフォーム (UWP) ゲームのコーディングの最初の手順は、アプリオブジェクトが Windows と対話できるようにするフレームワークを構築することです。これには、中断-再開イベント処理、ウィンドウフォーカスの変更、スナップなどの Windows ランタイム機能が含まれます。The first step in coding a Universal Windows Platform (UWP) game is building the framework that lets the app object interact with Windows, including Windows Runtime features such as suspend-resume event handling, changes in window focus, and snapping.

目的Objectives

  • ユニバーサル Windows プラットフォーム (UWP) DirectX ゲーム用のフレームワークを設定し、ゲームフロー全体を定義するステートマシンを実装します。Set up the framework for a Universal Windows Platform (UWP) DirectX game, and implement the state machine that defines the overall game flow.

注意

このトピックに従うには、ダウンロードした Simple3DGameDX サンプルゲームのソースコードを参照してください。To follow along with this topic, look in the source code for the Simple3DGameDX sample game that you downloaded.

はじめにIntroduction

Game プロジェクトのセットアップ 」トピックでは、 Wwinmain 関数と Iframeworkviewsource および IFrameworkView インターフェイスを紹介しました。In the Set up the game project topic, we introduced the wWinMain function as well as the IFrameworkViewSource and IFrameworkView interfaces. Appクラス (Simple3DGameDX プロジェクトのソースコードファイルで定義されて App.cpp います) は、 Simple3DGameDX ビュープロバイダーファクトリビュープロバイダーの両方として機能することを学習しました。We learned that the App class (which you can see defined in the App.cpp source code file in the Simple3DGameDX project) serves as both view-provider factory and view-provider.

このトピックでは、この記事から、 IFrameworkViewのメソッドを実装する必要があるゲームのAppクラスについてさらに詳しく説明します。This topic picks up from there, and goes into much more detail about how the App class in a game should implement the methods of IFrameworkView.

App:: Initialize メソッドThe App::Initialize method

アプリケーションの起動後、Windows が呼び出す最初のメソッドは、 IFrameworkView:: Initializeの実装です。Upon application launch, the first method that Windows calls is our implementation of IFrameworkView::Initialize.

実装では、これらのイベントをサブスクライブすることで、ゲームが中断 (および後で再開可能) イベントを処理できるようにするなど、UWP ゲームの最も基本的な動作を処理する必要があります。Your implementation should handle the most fundamental behaviors of a UWP game, such as making sure that the game can handle a suspend (and a possible later resume) event by subscribing to those events. ここではディスプレイアダプターデバイスにもアクセスできるため、デバイスに依存するグラフィックスリソースを作成できます。We also have access to the display adapter device here, so we can create graphics resources that depend on the device.

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>();
}

可能な場合は常に生のポインターを使用しないようにします (ほとんどの場合は可能です)。Avoid raw pointers whenever possible (and it's nearly always possible).

  • Windows ランタイム型の場合は、ポインターを完全に回避し、スタックに値を構築するだけでよく使用できます。For Windows Runtime types, you can very often avoid pointers altogether and just construct a value on the stack. ポインターが必要な場合は、 winrt:: com_ptr を使用します (これについては、すぐに例を示します)。If you do need a pointer, then use winrt::com_ptr (we'll see an example of that soon).
  • 一意のポインターの場合は、 std:: unique_ptrstd:: make_uniqueを使用します。For unique pointers, use std::unique_ptr and std::make_unique.
  • 共有ポインターの場合は、 std:: shared_ptrstd:: make_sharedを使用します。For shared pointers, use std::shared_ptr and std::make_shared.

App:: SetWindow メソッドThe App::SetWindow method

初期化後、Windows はIFrameworkView:: setwindowの実装を呼び出し、ゲームのメインウィンドウを表すcorewindowオブジェクトを渡します。After Initialize, Windows calls our implementation of IFrameworkView::SetWindow, passing a CoreWindow object representing the game's main window.

App:: SetWindowでは、ウィンドウ関連のイベントをサブスクライブし、いくつかのウィンドウと表示動作を構成します。In App::SetWindow, we subscribe to window-related events, and configure some window and display behaviors. たとえば、マウスとタッチの両方のコントロールで使用できるマウスポインターを ( CoreCursor クラスを使用して) 作成します。For example, we construct a mouse pointer (via the CoreCursor class), which can be used by both mouse and touch controls. また、ウィンドウオブジェクトをデバイスに依存するリソースオブジェクトに渡します。We also pass the window object to our device-dependent resources object.

イベントの処理については、 ゲームフロー管理 に関するトピックで詳しく説明します。We'll talk more about handling events in the Game flow management topic.

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 メソッドThe App::Load method

メインウィンドウが設定されたので、 IFrameworkView:: Load の実装が呼び出されます。Now that the main window is set, our implementation of IFrameworkView::Load is called. 読み込み は、 初期化setwindowよりもゲームデータまたはアセットを事前にフェッチするのに適しています。Load is a better place to pre-fetch game data or assets than Initialize and SetWindow.

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

ご覧のように、実際の作業は、ここで作成した GameMain オブジェクトのコンストラクターに委任されます。As you can see, the actual work is delegated to the constructor of the GameMain object that we make here. GameMainクラスは、およびで定義されてい GameMain.h GameMain.cpp ます。The GameMain class is defined in GameMain.h and GameMain.cpp.

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

GameMainコンストラクター (およびそれが呼び出すその他のメンバー関数) は、一連の非同期読み込み操作を開始して、ゲームオブジェクトの作成、グラフィックスリソースの読み込み、およびゲームのステートマシンの初期化を行います。The GameMain constructor (and the other member functions that it calls) begins a set of asynchronous loading operations to create the game objects, load graphics resources, and initialize the game's state machine. また、開始状態やグローバル値の設定など、ゲームを開始する前に必要な準備を行います。We also do any necessary preparations before the game begins, such as setting any starting states or global values.

Windows では、入力の処理を開始する前にゲームが実行できる時間に制限が設けられています。Windows imposes a limit on the time your game can take before it begins processing input. ここで説明するように asyc を使用すると、開始された作業がバックグラウンドで継続している間に、 読み込み をすばやく返すことができます。So using asyc, as we do here, means that Load can return quickly while the work that it has begun continues in the background. 読み込みに時間がかかる場合、または多くのリソースがある場合は、頻繁に更新される進行状況バーをユーザーに提供することをお勧めします。If loading takes a long time, or if there are lots of resources, then providing your users with a frequently updated progress bar is a good idea.

非同期プログラミングに慣れていない場合は、「 C++/WinRT を使用した同時実行と非同期操作」を参照してください。If you're new to asynchronous programming, then see Concurrency and asynchronous operations with 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 have focus. 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.
    ...
}

コンストラクターによって開始される一連の作業の概要を次に示します。Here's an outline of the sequence of work that's kicked off by the constructor.

  • GameRenderer型のオブジェクトを作成および初期化します。Create and initialize an object of type GameRenderer. 詳細については、「レンダリング フレームワーク I: レンダリングの概要」を参照してください。For more information, see Rendering framework I: Intro to rendering.
  • Simple3DGame型のオブジェクトを作成および初期化します。Create and initialize an object of type Simple3DGame. 詳細については、「メイン ゲーム オブジェクトの定義」を参照してください。For more information, see Define the main game object.
  • ゲーム UI コントロールオブジェクトを作成し、ゲーム情報オーバーレイを表示して、リソースファイルの読み込み時に進行状況バーを表示します。Create the game UI control object, and display game info overlay to show a progress bar as the resource files load. 詳細については、「ユーザー インターフェイスの追加」を参照してください。For more information, see Adding a user interface.
  • コントローラー (タッチ、マウス、または Xbox ワイヤレスコントローラー) からの入力を読み取るコントローラーオブジェクトを作成します。Create a controller object to read input from the controller (touch, mouse, or Xbox wireless controller). 詳細については、「コントロールの追加」を参照してください。For more information, see Adding controls.
  • [移動] コントロールと [カメラのタッチ] コントロールでは、画面の左下隅と右下隅にそれぞれ2つの四角形の領域を定義します。Define two rectangular areas in the lower-left and lower-right corners of the screen for the move and camera touch controls, respectively. プレーヤーは、( SetMoveRectの呼び出しで定義されている) 左下の四角形を仮想制御パッドとして使用して、カメラを前方および後方に移動します。The player uses the lower-left rectangle (defined in the call to SetMoveRect) as a virtual control pad for moving the camera forward and backward, and side to side. SetFireRectメソッドによって定義される右下の四角形は、ammo を起動するための仮想ボタンとして使用されます。The lower-right rectangle (defined by the SetFireRect method) is used as a virtual button to fire the ammo.
  • コルーチンを使用して、リソースの読み込みを別々のステージに分割します。Use coroutines to break resource loading into separate stages. Direct3D デバイスコンテキストへのアクセスは、デバイスコンテキストが作成されたスレッドに制限されます。オブジェクト作成のための Direct3D デバイスへのアクセスはフリースレッドです。Access to the Direct3D device context is restricted to the thread on which the device context was created; while access to the Direct3D device for object creation is free-threaded. その結果、 GameRenderer:: CreateGameDeviceResourcesAsync コルーチンは、完了タスク (GameRenderer:: FinalizeCreateGameDeviceResources) とは別のスレッドで実行できます。これは元のスレッドで実行されます。Consequently, the GameRenderer::CreateGameDeviceResourcesAsync coroutine can run on a separate thread from the completion task (GameRenderer::FinalizeCreateGameDeviceResources), which runs on the original thread.
  • Simple3DGame:: LoadLevelAsyncSimple3DGame:: FinalizeLoadLevelを使用して、レベルリソースの読み込みに同様のパターンを使用します。We use a similar pattern for loading level resources with Simple3DGame::LoadLevelAsync and Simple3DGame::FinalizeLoadLevel.

次のトピック (ゲームフロー管理) では、 GameMain:: 初期の状態がさらに表示されます。We'll see more of GameMain::InitializeGameState in the next topic (Game flow management).

App:: OnActivated 化メソッドThe App::OnActivated method

次に、 Coreapplicationview:: アクティブ化 イベントが発生します。Next, the CoreApplicationView::Activated event is raised. そのため、( App:: OnActivated 化メソッドなど) 使用しているonactivated 化されたイベントハンドラーが呼び出されます。So any OnActivated event handler that you have (such as our App::OnActivated method) is called.

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

ここで実行する作業は、主要な Corewindowをアクティブ化することだけです。The only work we do here is to activate the main CoreWindow. または、 App:: SetWindowでも選択できます。Alternatively, you can choose to do that in App::SetWindow.

App:: Run メソッドThe App::Run method

初期化setwindow、および Load によってステージが設定されました。Initialize, SetWindow, and Load have set the stage. これでゲームが開始され、 IFrameworkView:: Run の実装が呼び出されました。Now that the game is up and running, our implementation of IFrameworkView::Run is called.

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

ここでも、work は GameMainに委任されます。Again, work is delegated to GameMain.

GameMain:: Run メソッドThe GameMain::Run method

GameMain:: Run はゲームのメインループです。詳細については、「」を参照 GameMain.cpp してください。GameMain::Run is the main loop of the game; you can find it in GameMain.cpp. 基本的なロジックでは、ゲームのウィンドウが開いたままになっている間に、すべてのイベントをディスパッチし、タイマーを更新してから、グラフィックスパイプラインの結果をレンダリングして表示します。The basic logic is that while the window for your game remains open, dispatch all events, update the timer, and then render and present the results of the graphics pipeline. また、ゲームの状態間の遷移に使用されるイベントがディスパッチされて処理されます。Also here, the events used to transition between game states are dispatched and processed.

このコードは、ゲームエンジンのステートマシンの2つの状態にも関係しています。The code here is also concerned with two of the states in the game engine state machine.

  • UpdateEngineState::D eactivated 化されました。UpdateEngineState::Deactivated. これは、ゲームウィンドウが非アクティブ化される (フォーカスが失われた) か、またはスナップされることを指定します。This specifies that the game window is deactivated (has lost focus) or is snapped.
  • UpdateEngineState:: TooSmallUpdateEngineState::TooSmall. これは、クライアント領域が小さすぎてゲームをレンダリングできないことを指定します。This specifies that the client area is too small to render the game in.

これらのいずれの状態でも、ゲームはイベント処理を中断し、ウィンドウのフォーカス、unsnap、またはサイズの変更を待機します。In either of these states, the game suspends event processing, and waits for the window to focus, to unsnap, or to be resized.

ゲームにフォーカスがある場合、メッセージ キューに到達する各イベントを処理する必要があるため、CoreWindowDispatch.ProcessEventsProcessAllIfPresent オプションで呼び出す必要があります。When your game has focus, you must handle every event in the message queue as it arrives, and so you must call CoreWindowDispatch.ProcessEvents with the ProcessAllIfPresent option. 他のオプションを使用すると、メッセージイベントの処理に遅延が発生し、ゲームが応答しなくなったり、タッチ動作が遅くなる可能性があります。Other options can cause delays in processing message events, which can make your game feel unresponsive, or result in touch behaviors that feel sluggish.

ゲームが表示されない場合、中断されている場合、またはスナップされていない場合は、到着することのないメッセージをディスパッチするためにリソースサイクルを使用しないようにします。When the game is not visible, suspended, nor snapped, you don't want it to consume any resources cycling to dispatch messages that will never arrive. この場合、ゲームでは Processoneandallpending オプションを使用する必要があります。In this case, your game must use the ProcessOneAndAllPending option. このオプションは、イベントが取得されるまでブロックし、そのイベントを処理します (最初の処理中にプロセスキューに到達した他のイベントも処理されます)。That option blocks until it gets an event, and then processes that event (as well as any others that arrive in the process queue during the processing of the first). ProcessEvents は、キューが処理された後すぐに制御を戻します。CoreWindowDispatch.ProcessEvents then immediately returns after the queue has been processed.

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:: 初期化解除メソッドThe App::Uninitialize method

ゲームが終了すると、 IFrameworkView:: の初期化 の実装が呼び出されます。When the game ends, our implementation of IFrameworkView::Uninitialize is called. クリーンアップを実行するチャンスです。This is our opportunity to perform cleanup. アプリウィンドウを閉じると、アプリのプロセスが強制終了しません。代わりに、アプリシングルトンの状態がメモリに書き込まれます。Closing the app window doesn't kill the app's process; but instead it writes the state of the app singleton to memory. リソースの特別なクリーンアップを含め、システムによってこのメモリが解放されたときに特別な処理が行われる場合は、そのクリーンアップ用のコードを 初期化解除に配置します。If anything special must happen when the system reclaims this memory, including any special cleanup of resources, then put the code for that cleanup in Uninitialize.

ここでは、 App:: 初期化 解除は no op です。In our case, App::Uninitialize is a no-op.

void Uninitialize()
{
}

ヒントTips

独自のゲームを開発する場合は、このトピックで説明する方法を中心にスタートアップコードを設計します。When developing your own game, design your startup code around the methods described in this topic. 各メソッドの基本的な提案の簡単な一覧を次に示します。Here's a simple list of basic suggestions for each method.

  • Initializeを使用してメインクラスを割り当て、基本イベントハンドラーを接続します。Use Initialize to allocate your main classes, and connect up the basic event handlers.
  • Setwindowを使用して、ウィンドウ固有のイベントをサブスクライブし、メインウィンドウをデバイス依存のリソースオブジェクトに渡して、スワップチェーンの作成時にそのウィンドウを使用できるようにします。Use SetWindow to subscribe to any window-specific events, and to pass your main window to your device-dependent resources object so that it can use that window when creating a swap chain.
  • 残りのセットアップを処理し、オブジェクトの非同期作成とリソースの読み込みを開始するには、 Load を使用します。Use Load to handle any remaining setup, and to initiate the asynchronous creation of objects, and loading of resources. Procedurally によって生成されたアセットなど、一時ファイルまたはデータを作成する必要がある場合は、ここでも実行します。If you need to create any temporary files or data, such as procedurally generated assets, then do that here, too.

次の手順Next steps

このトピックでは、DirectX を使用する UWP ゲームの基本的な構造について説明しました。This topic has covered some of the basic structure of a UWP game that uses DirectX. これらのメソッドについては、後のトピックで説明します。そのため、これらのメソッドを念頭に置いておくことをお勧めします。It's a good idea to keep these methods in mind, because we'll be referring back to some of them in later topics.

次のトピック「ゲームフローの管理」では、ゲーム — Game flow management — の流れを維持するためにゲームの状態とイベント処理を管理する方法について詳しく説明します。In the next topic—Game flow management—we'll take an in-depth look at how to manage game states and event handling in order to keep the game flowing.