メイン ゲーム オブジェクトの定義
注意
このトピックは、「DirectX を使った単純なユニバーサル Windows プラットフォーム (UWP) ゲームの作成」チュートリアル シリーズの一部です。 リンク先のトピックでは、このシリーズのコンテキストを設定しています。
サンプル ゲームの基本的なフレームワークを構築し、高レベルのユーザーとシステムの動作を処理するステート マシンを実装したら、サンプル ゲームをゲームにするための規則とメカニズムを検討します。 ここでは、サンプル ゲームのメイン オブジェクトの詳細と、ゲーム ルールをゲーム ワールドとの対話式操作に変換する方法について説明します。
目標
- 基本的な開発手法を応用して、UWP DirectX ゲームのゲーム ルールと機構を実装する方法を学習します。
メイン ゲーム オブジェクト
Simple3DGameDX サンプル ゲームでは、Simple3DGame がメイン ゲーム オブジェクト クラスです。 Simple3DGame のインスタンスは、App::Load メソッドを介して間接的に構築されます。
Simple3DGame クラスの機能の一部を次に示します。
- ゲームプレイ ロジックの実装が含まれています。
- これらの詳細を伝達するメソッドが含まれています。
- アプリ フレームワークで定義されたステート マシンに対するゲーム状態の変更。
- アプリからゲーム オブジェクト自体へのゲーム状態の変更。
- ゲームの UI (オーバーレイとヘッドアップ ディスプレイ)、アニメーション、物理 (ダイナミクス) を更新するための詳細。
注意
グラフィックスの更新は GameRenderer クラスで処理します。これには、ゲームに使われるグラフィックス デバイス リソースを取得し、使うメソッドが含まれています。 詳細については、「レンダリング フレームワーク I: レンダリングの概要」を参照してください。
- 上位レベルでゲームをどのように定義するかに応じてゲームのセッション、レベル、または有効期間を定義するデータのコンテナーとして機能します。 この場合、ゲームの有効期間内はゲームの状態データが維持され、ユーザーがゲームを開始すると、一度初期化されます。
このクラスで定義されているメソッドとデータについては、後述する「Simple3DGame クラス」を参照してください。
ゲームを初期化して開始する
プレーヤーがゲームを開始すると、ゲーム オブジェクトはその状態を初期化し、オーバーレイの作成と追加を行い、プレーヤーのパフォーマンスを追跡する変数を設定して、レベルの構築時に使うオブジェクトをインスタンス化する必要があります。 このサンプルでは、この処理は App::Load に GameMain インスタンスが作成されたとき行われます。
GameMain::GameMain コンストラクターに Simple3DGame 型のゲーム オブジェクトが作成されます。 次に、GameMain::GameMain から呼び出される GameMain::ConstructInBackground fire-and-forget コルーチンの間に Simple3DGame::Initialize メソッドを使って初期化されます。
Simple3DGame::Initialize メソッド
サンプル ゲームでは、これらのコンポーネントをゲーム オブジェクトに設定しています。
- 新規のオーディオ再生オブジェクトを作成します。
- 一連のレベル プリミティブ、弾薬、障害物を含む、ゲームのグラフィック プリミティブの配列を作成します。
- ゲーム状態データを保存する場所を作成し、Game という名前を付け、ApplicationData::Current で指定するアプリ データ設定ストレージの場所に格納します。
- ゲーム タイマーと初期ゲーム内オーバーレイ ビットマップを作成します。
- 具体的なビュー パラメーターとプロジェクション パラメーター セットを使って新規のカメラを作成します。
- プレーヤーがコントロール開始位置とカメラ位置の 1 対 1 の対応を確保されるように、入力デバイス (コントローラー) をカメラと同じ位置に上下と左右の開始位置を設定します。
- プレーヤー オブジェクトを作成し、アクティブに設定します。 球体を使って、壁や障害物に近接するプレーヤーを検出したり、没入感を阻害するような位置にカメラが入り込むのを阻止したりします。
- ゲーム ワールド プリミティブを作成します。
- 円筒形の障害物を作成します。
- 標的 (Face オブジェクト) を作成し、番号を付けます。
- 弾薬の球体を作成します。
- レベルを作成します。
- ハイ スコアを読み込みます。
- 以前に保存されたゲーム状態を読み込みます。
ゲームには、ワールド、プレーヤー、障害物、標的、弾薬の球体の主要コンポーネントのインスタンスが既に存在します。 これらの全コンポーネントの設定と個々の固有レベルに対する動作を表すレベルのインスタンスもあります。 ゲームでどのようにレベルが構築されるのかを見てみましょう。
ゲーム レベルの構築と読み込み
レベル構築で苦労する処理の大部分は、サンプル ソリューションの GameLevels フォルダー内にある Level[N].h/.cpp
ファイルで行われます。 非常に特殊な実装に焦点を当てているため、ここでは説明しません。 重要な点は、各レベルのコードがそれぞれ個別の Level[N] オブジェクトとして実行されるということです。 ゲームを拡張する場合は、割り当てられている数字をパラメーターとして解釈し、障害物と標的を無作為に配置していた Level[N] オブジェクトを作成することができます。 または、リソース ファイルからレベルの設定データを読み込ませることもできます。インターネットからも入手可能です。
ゲームプレイを定義する
この時点でゲーム開発に必要なコンポーネントがすべて揃います。 レベルは、既にプリミティブに基づいてメモリ中に構築されており、プレーヤーが操作する準備ができています。
優れたゲームでは、プレーヤーからの入力に即座に反応し、瞬時のフィードバックを戻せることが条件となります。 これは、トゥイッチ アクションやリアルタイムのファーストパーソン シューティング ゲームをはじめ、ターン制の思考型の戦略ゲームに至るまで、あらゆる種類のゲームに言えることです。
Simple3DGame::RunGame メソッド
ゲーム レベルが進行している間、ゲームは Dynamics 状態です。
GameMain::Update は、次のように、1 フレームに 1 回アプリケーションの状態を更新するメインの更新ループです。 ゲーム状態が Dynamics である場合、この更新ループから Simple3DGame::RunGame メソッドを呼び出し、作業を処理します。
// 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 メソッドは、その状態の表現をレンダリングします。 これは GameMain::Update の呼び出しの直後に行われます。前のセクションで説明したように、これには状態を更新する Simple3DGame::RunGame が含まれています。
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 で定義されているパブリック メンバー関数には、次のものがあります。
- Initialize。 グローバル変数の開始値を設定し、ゲーム オブジェクトを初期化します。 詳細については、「ゲームを初期化して開始する」セクションを参照してください。
- LoadGame。 新しいレベルを初期化し、読み込みを開始します。
- LoadLevelAsync。 レベルを初期化し、レンダラー上で別のコルーチンを呼び出して、デバイス固有レベルのリソースを読み込むコルーチン。 このメソッドは独立したスレッドで実行されます。そのため、このスレッドから呼び出すことができるのは ID3D11Device メソッドだけです (ID3D11DeviceContext メソッドは呼び出されません)。 デバイス コンテキストのメソッドは、FinalizeLoadLevel メソッドで呼び出されます。 非同期プログラミングに慣れていない場合は、「C++/WinRT を使用した同時実行操作と非同期操作」を参照してください。
- FinalizeLoadLevel。 メイン スレッドで実行する必要があるレベル読み込みの作業を完了します。 これには、Direct3D 11 のデバイス コンテキスト (ID3D11DeviceContext) のメソッドの呼び出しが含まれます。
- StartLevel。 新しいレベルでゲームプレイを開始します。
- PauseGame。 ゲームを一時停止します。
- RunGame。 ゲーム ループの反復を実行します。 ゲームの状態が App::Update の場合、ゲーム ループを反復するごとに Active から 1 回呼び出されます。
- OnSuspending および OnResuming。 ゲームのオーディオをそれぞれ一時停止または再開します。
プライベート メンバー関数は次のとおりです。
- LoadSavedState および SaveState。 前者はゲームの現在の状態の読み込み、後者はこれを保存します。
- LoadHighScore と SaveHighScore。 前者はゲーム全体のハイ スコアを読み込み、後者はこれを保存します。
- InitializeAmmo。 弾として使われるそれぞれの球体の状態を各ラウンドの最初に元の状態に戻します。
- UpdateDynamics。 これは、アニメーションのキャンド ルーチンをはじめ、物理学とコントロール入力に基づいてゲーム オブジェクトをすべて更新するため、重要なメソッドになります。 これが、ゲームを定義するインタラクティビティの中核部分に相当します。 詳細については、「ゲーム ワールドを更新する」を参照してください。
これ以外のパブリック メソッドとして、表示用のアプリ フレームワークにゲーム プレイとオーバーレイ固有の情報を返すプロパティのアクセサーがあります。
データ メンバー
これらのオブジェクトは、ゲーム ループの実行に応じて更新されます。
- MoveLookController オブジェクト。 プレーヤーの入力を表します。 詳細については、「コントロールの追加」を参照してください。
- GameRenderer オブジェクト。 デバイス固有のオブジェクトとそのレンダリングを処理する Direct3D 11 レンダラーを表します。 詳細については、「レンダリング フレームワーク I」を参照してください。
- Audio オブジェクト。 ゲームのオーディオ再生をコントロールします。 詳細については、「サウンドの追加」を参照してください。
残りのゲーム変数には、プリミティブとゲーム内で対応するプリミティブの量を示すリストと、ゲーム プレイ固有のデータと制約が含まれます。
次の手順
実際のレンダリング エンジンについてはまだ説明していません。つまり、更新されたプリミティブで Render メソッドを呼び出した場合、これが画面上のピクセルでどのように表現されるのかについてです。 これらの側面については、「レンダリング フレームワーク I: レンダリングの概要」と「レンダリング フレームワーク II: ゲームのレンダリング」という 2 つのパートで説明します。 またプレーヤー コントロールによってゲーム状態がどのように更新されるのかについては、「コントロールの追加」を参照してください。