注
このトピックは、DirectX チュートリアル シリーズを使用して簡単なユニバーサル Windows プラットフォーム (UWP) ゲームを作成する
サンプル ゲームの基本的なフレームワークをレイアウトし、高レベルのユーザーとシステムの動作を処理するステート マシンを実装したら、サンプル ゲームをゲームに変えるルールとメカニズムを調べる必要があります。 サンプル ゲームのメイン オブジェクトの詳細と、ゲーム ルールをゲームの世界との対話に変換する方法を見てみましょう。
目標
- 基本的な開発手法を適用して、UWP DirectX ゲームのゲーム ルールとメカニズムを実装する方法について説明します。
メイン ゲーム オブジェクト
Simple3DGameDX サンプル ゲームでは、Simple3DGame がメイン ゲーム オブジェクト クラスです。 Simple3DGame のインスタンスは、App::Load メソッドを使用して間接的に構築されます。
Simple3DGame クラスの機能の一部を次に示します。
- ゲームプレイ ロジックの実装が含まれています。
- これらの詳細を伝えるメソッドが含まれています。
- ゲームの状態を、アプリ フレームワークで定義されているステート マシンに変更します。
- ゲームの状態をアプリからゲーム オブジェクト自体に変更します。
- ゲームの UI (オーバーレイとヘッドアップ ディスプレイ)、アニメーション、物理 (ダイナミクス) を更新するための詳細。
注
グラフィックスの更新は GameRenderer クラスによって処理されます。このクラスには、ゲームで使用されるグラフィックス デバイス リソースを取得して使用するメソッドが含まれています。 詳細については、「 レンダリング フレームワーク I: レンダリングの概要」を参照してください。
- ゲームを高レベルで定義する方法に応じて、ゲーム セッション、レベル、または有効期間を定義するデータのコンテナーとして機能します。 この場合、ゲーム状態データはゲームの有効期間中であり、ユーザーがゲームを起動したときに 1 回初期化されます。
このクラスで定義されているメソッドとデータを表示するには、以下 の Simple3DGame クラス を参照してください。
ゲームを初期化して開始する
プレイヤーがゲームを開始すると、ゲーム オブジェクトは状態を初期化し、オーバーレイを作成して追加し、プレイヤーのパフォーマンスを追跡する変数を設定し、レベルの構築に使用するオブジェクトをインスタンス化する必要があります。 このサンプルでは、 これは、GameMain インスタンスが App::Load で作成されるときに行われます。
Simple3DGame 型のゲーム オブジェクトは、GameMain::GameMain コンストラクターに作成されます。 その後、GameMain::ConstructInBackground fire-and-forget コルーチンの実行中に、Simple3DGame::Initialize メソッドを使用して初期化され、GameMain::GameMainにより呼び出されます。
Simple3DGame::Initialize メソッド
サンプル ゲームでは、ゲーム オブジェクトでこれらのコンポーネントを設定します。
- 新しいオーディオ再生オブジェクトが作成されます。
- レベル プリミティブ、弾薬、障害物の配列など、ゲームのグラフィック プリミティブの配列が作成されます。
- ゲーム状態データを保存するための場所が作成され、 Game という名前で、 ApplicationData::Current で指定されたアプリ データ設定の保存場所に配置されます。
- ゲーム タイマーと最初のゲーム内オーバーレイ ビットマップが作成されます。
- 新しいカメラは、特定のビューと投影パラメーターのセットを使用して作成されます。
- 入力デバイス (コントローラー) はカメラと同じ開始ピッチとヨーに設定されるため、プレイヤーは開始制御位置とカメラ位置の間に 1 対 1 の対応を持ちます。
- プレーヤー オブジェクトが作成され、アクティブに設定されます。 球オブジェクトを使用して、壁や障害物に対するプレイヤーの近接性を検出し、カメラが浸漬を中断する可能性のある位置に配置されないようにします。
- ゲーム ワールド プリミティブが作成されます。
- 円柱の障害物が作られました。
- ターゲット (Face オブジェクト) が作成され、番号が付けられます。
- 弾薬球が作成されます。
- レベルが作成されます。
- ハイスコアが読み込まれました。
- 以前に保存されたゲームの状態が読み込まれます。
ゲームには、世界、プレイヤー、障害物、ターゲット、弾薬球など、すべての主要コンポーネントのインスタンスが追加されました。 また、上記のすべてのコンポーネントの構成と、特定のレベルごとの動作を表すレベルのインスタンスもあります。 次に、ゲームがレベルを構築する方法を見てみましょう。
ゲーム レベルをビルドして読み込む
レベル構築の大部分の作業は、サンプルソリューションの Level[N].h/.cpp
フォルダーにある ファイルで行われます。 非常に具体的な実装に焦点を当てているため、ここではそれらをカバーしません。 重要なのは、各レベルのコードが個別の Level[N] オブジェクトとして実行されていることです。 ゲームを拡張する場合は、割り当てられた数値をパラメーターとして受け取り、障害物とターゲットをランダムに配置する Level[N] オブジェクトを作成できます。 または、リソース ファイルまたはインターネットからレベルの構成データを読み込むことができます。
ゲームプレイを定義する
この時点で、ゲームを開発するために必要なすべてのコンポーネントがあります。 レベルはプリミティブからメモリ内に構築されており、プレイヤーが対話を開始する準備ができています。
最高のゲームはプレイヤーの入力に即座に反応し、即座にフィードバックを提供します。 これは、ツイッチアクション、リアルタイムの一人称シューティングゲームから、思慮深いターンベースの戦略ゲームまで、あらゆる種類のゲームに当てはまります。
Simple3DGame::RunGame メソッド
ゲームレベルが進行中の間、ゲームはダイナミクス 状態にあります。
GameMain::Update は、次に示すように、アプリケーションの状態をフレームごとに 1 回更新するメインの更新ループです。 更新ループは Simple3DGame::RunGame メソッドを呼び出して、ゲームが Dynamics 状態の場合に作業を処理します。
// Updates the application state once per frame.
void GameMain::Update()
{
// The controller object has its own update loop.
m_controller->Update();
switch (m_updateState)
{
...
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)
{
...
Simple3DGame::RunGame は、ゲーム ループの現在のイテレーションに対するゲーム プレイの現在の状態を定義するデータのセットを処理します。
Simple3DGame::RunGame のゲーム フロー ロジックを次に示します。
- このメソッドは、レベルが完了するまでの秒数をカウントダウンするタイマーを更新し、レベルの時間の有効期限が切れているかどうかをテストします。 これはゲームのルールの 1 つです。時間がなくなると、すべてのターゲットが撃たれていなければ、ゲームオーバーになります。
- 時間がなくなった場合、メソッドは TimeExpired ゲームの状態を設定し、前のコードの Update メソッドに戻ります。
- 時間が残っている場合は、ムーブルックコントローラがカメラ位置の更新のためにポーリングされます。具体的には、カメラ平面 (プレイヤーが見ている場所) から投影されるビューの通常の角度と、コントローラーが最後にポーリングされてから移動した角度の距離に対する更新。
- カメラは、ムーブルック コントローラーからの新しいデータに基づいて更新されます。
- ダイナミクス、またはプレイヤーコントロールに依存しないゲームワールド内のオブジェクトのアニメーションと動作が更新されます。 このサンプル ゲームでは、 Simple3DGame::UpdateDynamics メソッドを呼び出して、発射された弾薬球の動き、柱の障害物のアニメーション、ターゲットの動きを更新します。 詳細については、「 ゲームの世界を更新する」を参照してください。
- このメソッドは、レベルが正常に完了した条件が満たされているかどうかを確認します。 その場合は、レベルのスコアを最終処理し、これが最後のレベル (6) であるかどうかを確認します。 最後のレベルの場合、このメソッドは GameState::GameComplete ゲームの状態を返します。それ以外の場合は、 GameState::LevelComplete ゲームの状態を返します。
- レベルが完了していない場合、メソッドはゲームの状態を GameState::Active に設定し、返します。
ゲームの世界を更新する
このサンプルでは、ゲームの実行中に Simple3DGame::UpdateDynamics メソッドを Simple3DGame::RunGame メソッド ( GameMain::Update から呼び出されます) から呼び出して、ゲーム シーンにレンダリングされるオブジェクトを更新します。
UpdateDynamics などのループは、プレイヤーの入力に関係なく、ゲームワールドを動かすために使用されるすべてのメソッドを呼び出して、イマーシブ なゲーム エクスペリエンスを作成し、レベルを生き生きとさせます。 これには、レンダリングする必要があるグラフィックスや、プレーヤーの入力がない場合でも動的な世界を実現するためのアニメーション ループの実行が含まれます。 あなたのゲームでは、風に揺れる木や、海岸沿いに押し寄せる波、煙を出す機械、そして伸びながら動き回るエイリアンモンスターが含まれるかもしれません。 また、プレイヤーの球と世界の衝突を含むオブジェクト間の相互作用、または弾薬と障害物とターゲットの間の相互作用も含まれます。
ゲームが特に一時停止されている場合を除き、ゲーム ループはゲームワールドの更新を続ける必要があります。それがゲーム ロジック、物理アルゴリズムに基づいているかどうか、または単純なランダムであるかどうか。
サンプルゲームでは、この原理は ダイナミクスと呼ばれ、柱の障害物の上昇と落下、および発射中および運動中の弾薬球の動きと物理的な動作を包含しています。
Simple3DGame::UpdateDynamics メソッド
このメソッドは、これら 4 つの計算セットを処理します。
- 世界の中で発射された弾薬球の位置。
- 柱の障害物のアニメーション。
- プレイヤーと世界境界の交差部分。
- 弾薬球が障害物、ターゲット、他の弾薬球、および世界と衝突すること。
障害物のアニメーションは、 Animate.h/.cpp ソース コード ファイルで定義されたループで行われます。 弾薬と衝突の動作は、コードで提供され、重力や素材の特性を含むゲーム世界のグローバル定数のセットによってパラメーター化された、簡略化された物理アルゴリズムによって定義されます。 これはすべてゲーム ワールド座標空間で計算されます。
フローを確認する
シーン内のすべてのオブジェクトを更新し、衝突を計算したので、この情報を使用して、対応する視覚的な変更を描画する必要があります。
GameMain::Update がゲーム ループの現在のイテレーションを完了した後、サンプルは直ちに GameRenderer::Render を呼び出して更新されたオブジェクト データを取得し、次に示すようにプレイヤーに提示する新しいシーンを生成します。
void GameMain::Run()
{
while (!m_windowClosed)
{
if (m_visible)
{
switch (m_updateState)
{
case UpdateEngineState::Deactivated:
case UpdateEngineState::TooSmall:
...
// Otherwise, fall through and do normal processing to perform rendering.
default:
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
CoreProcessEventsOption::ProcessAllIfPresent);
// GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
// state, uses Simple3DGame::UpdateDynamics to update game world.
Update();
// Render is called immediately after the Update loop.
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.
}
ゲーム ワールドのグラフィックスをレンダリングする
ゲーム内のグラフィックスは、理想的にはメインゲームループが繰り返し実行されるのと同じ頻度で更新することをお勧めします。 ループが繰り返されると、プレイヤー入力の有無にかかわらず、ゲーム ワールドの状態が更新されます。 これにより、計算されたアニメーションと動作をスムーズに表示できます。 プレイヤーがボタンを押したときにのみ動く単純な水のシーンがあるとします。 それは現実的ではないでしょう。良いゲームは、常に滑らかで流動的に見えます。
GameMain::Run で上に示したように、サンプル ゲームのループを思い出してください。 ゲームのメイン ウィンドウが表示され、スナップまたは非アクティブ化されていない場合、ゲームはその更新の結果を更新してレンダリングし続けます。 次に調べる GameRenderer::Render メソッドは、その状態の表現をレンダリングします。 これは、前のセクションで説明したように、状態を更新するための Simple3DGame::RunGame を含む GameMain::Update の呼び出しの直後に行われます。
GameRenderer::Render は 3D ワールドのプロジェクションを描画し、その上に Direct2D オーバーレイを描画します。 完了すると、最終的なスワップ チェーンと、表示用の結合バッファーが表示されます。
注
サンプル ゲームの Direct2D オーバーレイには 2 つの状態があります。1 つは、ゲームが一時停止メニューのビットマップを含むゲーム情報オーバーレイを表示し、もう 1 つはタッチスクリーンのムーブルック コントローラーの四角形と共に十字線を表示する状態です。 スコア テキストは両方の状態で描画されます。 詳細については、「レンダリング フレームワーク I: レンダリングの概要」を参照してください。
GameRenderer::Render メソッド
void GameRenderer::Render()
{
bool stereoEnabled{ m_deviceResources->GetStereoState() };
auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };
...
if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
{
// This section is only used after the game state has been initialized and all device
// resources needed for the game have been created and associated with the game objects.
...
for (auto&& object : m_game->RenderObjects())
{
object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
}
}
d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
d2dContext->BeginDraw();
// To handle the swapchain being pre-rotated, set the D2D transformation to include it.
d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());
if (m_game != nullptr && m_gameResourcesLoaded)
{
// This is only used after the game state has been initialized.
m_gameHud.Render(m_game);
}
if (m_gameInfoOverlay.Visible())
{
d2dContext->DrawBitmap(
m_gameInfoOverlay.Bitmap(),
m_gameInfoOverlayRect
);
}
...
}
}
Simple3DGame クラス
これらは、 Simple3DGame クラスによって定義されるメソッドとデータ メンバーです。
メンバー関数
Simple3DGame によって定義されるパブリック メンバー関数には、以下のものが含まれます。
- 初期化。 グローバル変数の開始値を設定し、ゲーム オブジェクトを初期化します。 これについては、「ゲームの 初期化と開始 」セクションで説明します。
- LoadGameをします。 新しいレベルを初期化し、読み込みを開始します。
-
を LoadLevelAsyncします。 レベルを初期化し、レンダラーで別のコルーチンを呼び出してデバイス固有のレベル リソースを読み込むコルーチン。 このメソッドは別のスレッドで実行されます。その結果、このスレッドから呼び出すことができるのは、(ID3D11DeviceContext メソッドではなく) ID3D11Device メソッドだけです。 すべてのデバイス コンテキスト メソッドは FinalizeLoadLevel メソッドで呼び出されます。 非同期プログラミングを初めて使用する場合は、C++/WinRT
コンカレンシーと非同期操作を参照してください。 - FinalizeLoadLevelを
します。 メイン スレッドで実行する必要があるレベル読み込みの作業を完了します。 これには、Direct3D 11 デバイス コンテキスト (ID3D11DeviceContext) メソッドの呼び出しが含まれます。 - はレベルを開始します。 新しいレベルのゲームプレイを開始します。
- を PauseGameします。 ゲームを一時停止します。
- RunGameを
します。 ゲーム ループのイテレーションを実行します。 これは、 App::Update から、ゲームの状態がアクティブな場合に、ゲーム ループの反復ごとに 1 回呼び出されます。 - OnSuspending と OnResumingです。 ゲームのオーディオをそれぞれ一時停止または再開します。
プライベート メンバー関数を次に示します。
- LoadSavedState および SaveState。 ゲームの現在の状態をそれぞれ読み込む/保存します。
- LoadHighScore を
し、SaveHighScoreを します。 ゲームごとのハイスコアを読み込み/保存します。 - InitializeAmmoを
します。 弾薬として使用される各球オブジェクトの状態を、各ラウンドの開始時の元の状態にリセットします。 - UpdateDynamicsを
に更新します。 これは、プリセットのアニメーションルーチン、物理演算、およびコントロール入力に基づいてすべてのゲームオブジェクトを更新するため、重要です。 これは、ゲームを定義する対話機能の中心です。 これについては、「 ゲーム ワールドの更新 」セクションで説明します。
他のパブリック メソッドは、ゲームプレイ固有の情報とオーバーレイ固有の情報を表示用のアプリ フレームワークに返すプロパティ アクセサーです。
データ メンバー
これらのオブジェクトは、ゲーム ループの実行時に更新されます。
- MoveLookController オブジェクト。 プレーヤーの入力を表します。 詳細については、「コントロールの追加」を参照してください。
- GameRenderer オブジェクト。 デバイス固有のすべてのオブジェクトとそのレンダリングを処理する Direct3D 11 レンダラーを表します。 詳細については、「レンダリング フレームワーク I」を参照してください。
- オーディオ オブジェクト。 ゲームのオーディオ再生を制御します。 詳細については、「サウンドの追加」を参照してください。
ゲーム変数の残りの部分には、プリミティブのリストと、それぞれのゲーム内の量が含まれており、ゲーム プレイ固有のデータと制約があります。
次のステップ
私たちはまだ実際のレンダリングエンジンについて話していません—更新されたプリミティブの Render メソッドへの呼び出しがどのようにしてあなたの画面上のピクセルに変換されるのかについてです。 これらの側面は、レンダリング フレームワーク I: レンダリングの概要とレンダリングフレームワーク II: ゲーム レンダリングの 2 つの部分で説明されています。 プレイヤー コントロールがゲームの状態を更新する方法に関心がある場合は、「 コントロールの追加」を参照してください。