注
このトピックは、DirectX チュートリアル シリーズを使用して簡単なユニバーサル Windows プラットフォーム (UWP) ゲームを作成する
ユニバーサル Windows プラットフォーム (UWP) ゲームのコーディングの最初の手順は、中断再開イベント処理、ウィンドウの可視性の変更、スナップなどの Windows ランタイム機能など、アプリ オブジェクトが Windows と対話できるようにするフレームワークを構築することです。
目標
- ユニバーサル Windows プラットフォーム (UWP) DirectX ゲームのフレームワークを設定し、ゲーム フロー全体を定義するステート マシンを実装します。
注
このトピックに従うには、ダウンロードした Simple3DGameDX サンプル ゲームのソース コードを参照してください。
イントロダクション
このトピックでは、そこから取り上げ、ゲーム内の
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_ptr と std::make_uniqueを使用します。
- 共有ポインターの場合は、std::shared_ptr
使用し、std::make_shared します。
App::SetWindow メソッド
初期化後、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 の実装が呼び出されます。 読み込み は、初期化 や SetWindowを行うよりも、ゲームデータやアセットを事前にフェッチするのに適しています。
void Load(winrt::hstring const& /* entryPoint */)
{
if (!m_main)
{
m_main = winrt::make_self<GameMain>(m_deviceResources);
}
}
ご覧のように、実際の作業は、ここで作成する GameMain オブジェクトのコンストラクターに任されます。
GameMain クラスは、GameMain.h
および GameMain.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::LoadLevelAsync と Simple3DGame::FinalizeLoadLevelを使用してレベル リソースを読み込む場合にも同様のパターンを使用します。
GameMain::InitializeGameState の詳細については、次のトピック (ゲーム フロー管理) を参照してください。
App::OnActivated メソッド
次に、CoreApplicationView::Activated イベントが発生します。 そのため、(例えば、App::OnActivated メソッドのような) OnActivated イベント ハンドラーが呼び出されます。
void OnActivated(CoreApplicationView const& /* applicationView */, IActivatedEventArgs const& /* args */)
{
CoreWindow window = CoreWindow::GetForCurrentThread();
window.Activate();
}
ここで行う唯一の作業は、CoreWindowのメイン
App::Run メソッド
初期化、SetWindow、および Load によってステージが設定されています。 ゲームが起動して実行できるようになったので、IFrameworkView::Run の実装が呼び出されます。
void Run()
{
m_main->Run();
}
ここでも、作業は GameMainに委任されます。
GameMain::Run メソッド
GameMain::Run はゲームのメイン ループです。GameMain.cpp
で見つけることができます。 基本的なロジックは、ゲームのウィンドウが開いたままで、すべてのイベントをディスパッチし、タイマーを更新してから、グラフィックス パイプラインの結果をレンダリングして表示することです。 また、ここでは、ゲームの状態間の遷移に使用されるイベントがディスパッチされ、処理されます。
ここでのコードは、ゲーム エンジンステートマシンの 2 つの状態にも関係しています。
UpdateEngineState::Deactivated をします。 これは、ゲーム ウィンドウが非アクティブ化 (フォーカスが失われた) か、スナップされていることを指定します。 - UpdateEngineState::TooSmallを
します。 これにより、クライアント領域が小さすぎてゲームをレンダリングできなくなることを指定します。
これらの状態のいずれかで、ゲームはイベント処理を中断し、ウィンドウのアクティブ化、スナップ解除、またはサイズ変更を待機します。
ゲーム ウィンドウが表示されている間 (
ゲームが 表示されない (Window.Visible が false
されている) 場合、または中断されている場合、あるいはサイズが小さすぎる(スナップされている)場合、到着しないメッセージを処理するためにリソースを消費しないようにします。 この場合、ゲームでは 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 は no-opです。
void Uninitialize()
{
}
ヒント
独自のゲームを開発するときは、このトピックで説明するメソッドを中心にスタートアップ コードを設計します。 各メソッドの基本的な提案の簡単な一覧を次に示します。
- Initialize を使用してメイン クラスを割り当て、基本的なイベント ハンドラーを接続します。
- SetWindow を使用して、ウィンドウ固有のイベントをサブスクライブし、メイン ウィンドウをデバイス依存リソース オブジェクトに渡して、スワップ チェーンの作成時にそのウィンドウを使用できるようにします。
- Load を使用して、残りのセットアップを処理し、オブジェクトの非同期作成とリソースの読み込みを開始します。 手続き型に生成された資産など、一時ファイルやデータを作成する必要がある場合は、ここでも作成します。
次のステップ
このトピックでは、DirectX を使用する UWP ゲームの基本的な構造の一部について説明しました。 これらのメソッドは、後のトピックでいくつか紹介するので、念頭に置いておくことをお勧めします。
次のトピックゲーム フロー管理では、ゲームの流れを維持するために、ゲームの状態とイベント処理を管理する方法について詳しく説明します。