C++ と Microsoft* DirectX* で Windows* 8 向け 3D ゲームを開発する

同カテゴリーの次の記事

GCC 5.0 での x86 向けの最適化新機能

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Developing 3D Games for Windows* 8 with C++ and Microsoft DirectX*」の日本語参考訳です。


ゲーム開発は常に注目され続けています: ゲームの人気は高く、さまざまなパッケージの売れ筋ランキングでトップを占めています。優れたゲームを開発するためには、パフォーマンスが絶対条件です。誰だって、たとえローエンドのデバイスであっても、遅いゲームや不具合があるゲームはプレイしたくないでしょう。

さまざまな言語とフレームワークを使用してゲームを開発できますが、優れたパフォーマンスの Windows* ゲームを開発するには、Microsoft* DirectX* と C++ の組み合わせに勝るものはないでしょう。これらのテクノロジーを利用することで、ハードウェアの能力を最大限に活用し、優れたパフォーマンスを得ることができます。

私は通常 C# で開発を行っていますが、これらのテクノロジーを利用してゲームを開発してみることにしました。過去には C++ で多くの開発を行っていましたが、当時と比べると C++ 言語はかなり進化しています。また、これまで DirectX* は全く使用したことがないため、この記事では初心者の視点にたってゲーム開発について述べます。経験豊富な開発者の皆さんには、ミスが目につくかもしれませんが、どうかご容赦ください。

この記事は、サッカーのペナルティー・キック・ゲームの開発方法を紹介します。ゲームがボールをキックし、ユーザーがゴールキーパーを動かしてキャッチします。ここではゼロから開発するのではなく、Windows* 8.1 向けゲーム開発のスタートポイントというべき、Microsoft* Visual Studio* 3D Starter Kit を利用します。

Microsoft* Visual Studio* 3D Starter Kit

Starter Kit をダウンロードしたら、フォルダーに展開し、StarterKit.sln ファイルを開きます。このソリューションには、すぐに実行可能な Windows 8.1 C++ プロジェクトが含まれています。プロジェクトを実行すると、図 1 のような出力が得られます。


図 1. Microsoft* Visual Studio* 3D Starter Kit の初期状態

この Starter Kit プログラムは、いくつかの役立つ概念を示しています。

  • 5 つのオブジェクトが描画され、”踊る” ティーポットの周りを 4 つの形状の物体が回転します。
  • 各物体のマテリアルは異なり、無地のものもあれば、立方体のようにビットマップが指定されているものもあります。
  • 光源は画面左上です。
  • 右下には FPS (1 秒あたりのフレーム数) カウンターが表示されます。
  • スコアボードは上部に配置されています。
  • オブジェクトをクリックすると、そのオブジェクトがハイライトされ、スコアが上がります。
  • ゲーム上で右クリックするか、画面下部から上方向にスワイプするとアプリバーが表示され、ティーポットの色を切り替える 2 つのボタンを利用できます。

これらの機能を利用してさまざまなゲームを開発できますが、最初に Starter Kit に含まれるファイルを確認してみましょう。

まず、App.xaml とそれに対応する cpp/h を見てみます。App.xaml アプリケーションを起動すると、DirectXPage が実行されます。DirectXPage.xaml には、SwapChainPanel とアプリバーがあります。SwapChainPanel は、XAML ページ上の DirectX* グラフィックス用のホスティング・サーフェスです。Microsoft* Direct3D* シーンで利用可能な XAML オブジェクトを追加できます。独自にコントロールを作成しなくても、DirectX* ゲームにボタン、ラベル、その他の XAML オブジェクトを追加できるので便利です。Starter Kit には、スコアボードとして使用可能な StackPanel もあります。

DirectXPage.xaml.cpp には、変数の初期化、サイズと向きを変更するためのイベントハンドラーのフック、アプリバーボタンの Click イベントのハンドル、レンダリング・ループがあります。すべての XAML オブジェクトは、通常の Windows* 8 プログラムと同様に扱われます。このファイルは、オブジェクトをタップ (またはマウスでクリック) したかどうかチェックする Tapped イベントも処理します。タップ (またはクリック) された場合は、そのオブジェクトのスコアをインクリメントします。

SwapChainPanel が DirectX* コンテンツをレンダリングすべきであることをプログラムに知らせる必要があります。その方法としてドキュメントでは、「SwapChainPanel インスタンスを IInspectable または IUnknown にキャストしてから、QueryInterface を呼び出して ISwapChainPanelNative インターフェイス (SwapChainPanel を補完し、プロセス間ブリッジを有効にするネイティブ・インターフェイス実装) への参照を取得します。そして、その参照に対して ISwapChainPanelNative::SetSwapChain を呼び出し、スワップチェーンと SwapChainPanel インスタンスを関連付けます。」と説明しています。これは、DeviceResources.cpp にある CreateWindowSizeDependentResources メソッドで行われます。

ページと FPS カウンターのレンダリングを行うゲームのメインループは StarterKitMain.cpp にあります。

Game.cpp には、ゲームループとヒットテストがあります。Update メソッドのアニメーションを計算し、Render メソッドのすべてのオブジェクトを描画します。FPS カウンターは SampleFpsTextRenderer.cpp でレンダリングされます。

ゲームのオブジェクトは アセットフォルダーにあります。Teapot.fbx にはティーポットが、GameLevel.fbx には踊るティーポットの周りを回転する 4 つの形状の物体が含まれています。

Starter Kit サンプルアプリについて基本的なことが分かったところで、独自のゲームの開発に取り掛かりましょう。

ゲームにアセットを追加する

ここではサッカーゲームを開発するため、最初のアセットとしてサッカーボールを Gamelevel.fbx に追加します。まず、このファイルにある 4 つの形状の物体を削除します。各物体を選択して Delete キーを押すと削除できます。[ソリューション エクスプローラー] で、CubeUVImage.png も削除します。これは、削除した立方体で使用されているテクスチャーなので不要です。

次に、球体をモデルに追加します。ツールボックスを開き (表示されていない場合は [表示] > [ツールボックス] を選択します)、球体をダブルクリックしてモデルに追加します。ボールが小さい場合は、エディター上部のツールバーにあるズームボタンをクリックするか、Z キーを押してからマウスをドラッグするか (中心から外側に向けてドラッグすると拡大)、あるいは上矢印/下矢印をタップして拡大できます。また、Ctrl キーを押しながらマウスホイールを使って拡大することもできます。図 2 のような画面が表示されます。


図 2. 球体が表示されたモデルエディター

球体は白色で光が当たっているだけなので、サッカーボールのテクスチャーを追加する必要があります。最初に、図 3 のような六角形のグリッドを適用してみました。


図 3. ボールのテクスチャーに六角形のグリッドを使用 (最初の試み)

球体にテクスチャーを適用するには、そのテクスチャーを選択し、[プロパティ] ウィンドウで .png ファイルを Texture1 プロパティーに割り当てます。これは良いアイデアのように思われましたが、図 4 のような結果になりました。


図 4. テクスチャー適用後の球体

球体にテクスチャーを投影すると六角形が変形してしまいます。そのため、図 5 のように変形したテクスチャーが必要です。


図 5. 球体に適用されたサッカーボールのテクスチャー

このテクスチャーを適用すると、球体がよりサッカーボールらしくなります。いくつかのプロパティーを調整して、より本物のサッカーボールに近くなるようにします。ボールを選択し、[プロパティ] ウィンドウでフォン効果を編集します。フォン光源モデルにはディレクショナル・ライトとアンビエント・ライトが含まれ、オブジェクトの反射プロパティーをシミュレーションします。これは Visual Studio* に含まれるシェーダーで、ツールボックスからドラッグできます。シェーダーの詳細と Visual Studio* シェーダーデザイナーでシェーダーを作成する方法については、「参考資料」にあるリンクを参照してください。MaterialSpecular と MaterialSpecularPower のプロパティーをそれぞれ 0.2 と 16 に設定します。これでより本物のサッカーボールらしくなります (図 6)。


図 6. 完成したサッカーボール

Visual Studio* で独自にモデルをデザインしたくない場合は、Web から規定のモデルを利用できます。Visual Studio* では、FBX、DAE、OBJ 形式のモデルが許可されています。これらのモデルは、ソリューションのアセットに追加するだけで利用できます。例えば、図 7 のような .obj ファイル (http://www.turbosquid.com からダウンロードした無料のモデル) を使用できます。


図 7. .obj 形式の 3D ボールモデル

モデルにアニメーションを追加する

モデルの準備が完了したら、モデルにアニメーションを追加します。その前に、このゲームではティーポットは不要なので削除しましょう。アセットフォルダーで teapot.fbx を削除します。次に、このモデルのロード処理とアニメーションも削除します。モデルのロード処理は、Game.cppCreateDeviceDependentResources で非同期に行われています。

// シーン・オブジェクトのロード
auto loadMeshTask = Mesh::LoadFromFileAsync(
	m_graphics,
	L"gamelevel.cmo",
	L"",
	L"",
	m_meshModels)
	.then([this]()
{
	// 別のファイルからティーポットをロードし、メッシュのベクトルに追加します。
	return Mesh::LoadFromFileAsync(

モデルを変更し、タスクの継続処理を削除し、ボールのみがロードされるようにする必要があります。

void Game::CreateDeviceDependentResources()
{
	m_graphics.Initialize(m_deviceResources->GetD3DDevice(),
		m_deviceResources->GetD3DDeviceContext(),
		m_deviceResources->GetDeviceFeatureLevel());

	// DirectX が三角形を選択しないように設定し、
	// 常にメッシュ全体が表示されるようにします。
	CD3D11_RASTERIZER_DESC d3dRas(D3D11_DEFAULT);
	d3dRas.CullMode = D3D11_CULL_NONE;
	d3dRas.MultisampleEnable = true;
	d3dRas.AntialiasedLineEnable = true;

	ComPtr<ID3D11RasterizerState> p3d3RasState;
	m_deviceResources->GetD3DDevice()->CreateRasterizerState(&d3dRas,
		&p3d3RasState);
	m_deviceResources->GetD3DDeviceContext()->RSSetState(p3d3RasState.Get());

	// シーン・オブジェクトをロードします。
	auto loadMeshTask = Mesh::LoadFromFileAsync(
		m_graphics,
		L"gamelevel.cmo",
		L"",
		L"",
		m_meshModels);
		

	(loadMeshTask).then([this]()
	{
		// シーンのレンダリング準備完了
		m_loadingComplete = true;
	});
}

対応する ReleaseDeviceDependentResources では、メッシュをクリアするだけです。

void Game::ReleaseDeviceDependentResources()
{
	for (Mesh* m : m_meshModels)
	{
		delete m;
	}
	m_meshModels.clear();

	m_loadingComplete = false;
}

次に、ボールだけが回転するように Update メソッドを変更します。

void Game::Update(DX::StepTimer const& timer)
{
	// シーンの回転
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
}

回転速度は、乗数 (0.5f) を変更することで調整できます。より速く回転させるには、この乗数を大きくします。この乗数は、ボールが毎秒 0.5/(2 * pi) ラジアンの角度で回転することを示します。Render メソッドは、任意の回転でボールをレンダリングします。

void Game::Render()
{
	// 非同期にロードし、ロード後に形状を描画します。
	if (!m_loadingComplete)
	{
		return;
	}

	auto context = m_deviceResources->GetD3DDeviceContext();

	// レンダリング・ターゲットを画面に設定します。
	auto rtv = m_deviceResources->GetBackBufferRenderTargetView();
	auto dsv = m_deviceResources->GetDepthStencilView();
	ID3D11RenderTargetView *const targets[1] = { rtv };
	context->OMSetRenderTargets(1, targets, dsv);

	// シーンモデルを描画します。
	XMMATRIX rotation = XMMatrixRotationY(m_rotation);
	for (UINT i = 0; i < m_meshModels.size(); i++)
	{
		XMMATRIX modelTransform = rotation;

		String^ meshName = ref new String(m_meshModels[i]->Name());
	
		m_graphics.UpdateMiscConstants(m_miscConstants);

		m_meshModels[i]->Render(m_graphics, modelTransform);
	}
}

ToggleHitEffect は、このゲームでは何も行いません。つまり、ボールをタッチしても光りません。

void Game::ToggleHitEffect(String^ object)
{
	
}

ボールを光るようにしなくても、タッチされたことが分かるように、OnHitObject を次のように変更します。

String^ Game::OnHitObject(int x, int y)
{
	String^ result = nullptr;

	XMFLOAT3 point;
	XMFLOAT3 dir;
	m_graphics.GetCamera().GetWorldLine(x, y, &point, &dir);

	XMFLOAT4X4 world;
	XMMATRIX worldMat = XMMatrixRotationY(m_rotation);
	XMStoreFloat4x4(&world, worldMat);

	float closestT = FLT_MAX;
	for (Mesh* m : m_meshModels)
	{
		XMFLOAT4X4 meshTransform = world;

		auto name = ref new String(m->Name());

		float t = 0;
		bool hit = HitTestingHelpers::LineHitTest(*m, &point, &dir,
			&meshTransform, &t);
		if (hit && t < closestT)
		{
			result = name;
		}
	}

	return result;
}

プロジェクトを実行すると、ボールが y 軸上を回転するのを確認できます。次に、ボールを動かしてみましょう。

ボールを動かす

ボールを動かすには、例えば上下に移動するなど、動きを指定しなければなりません。最初に、Game.h で現在の位置を格納する変数を宣言する必要があります。

class Game
{
public:
	// コードスニペット
private:
	// コードスニペット
	float m_translation;

Update メソッドで現在の位置を計算します。

void Game::Update(DX::StepTimer const& timer)
{
	// シーンの回転
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
	const float maxHeight = 7.0f;
	auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
	m_translation = totalTime > 1.0f ? 
		maxHeight - (maxHeight * (totalTime - 1.0f)) : maxHeight *totalTime;
}

これで、ボールが 2 秒ごとに上下に移動するようになります。最初は上に移動し、次に下に移動します。Render メソッドは、生成される行列を計算し、ボールを新しい位置にレンダリングします。

void Game::Render()
{
	// コードスニペット

	// シーンモデルを描画します。
	XMMATRIX rotation = XMMatrixRotationY(m_rotation);
	rotation *= XMMatrixTranslation(0, m_translation, 0);

プロジェクトを実行すると、一定の間隔でボールが上下に動くのを確認できます。次に、ボールに物理特性を追加します。

ボールに物理特性を追加する

ボールに物理特性を追加するには、重力をシミュレーションする必要があります。物理の授業で、次の重力加速度の式を見た覚えはありませんか?

s = s0 + v0t + 1/2at2

v = v0 + at

st 時点での位置、s0 は初期位置、v0 は初期速度、a は加速度です。垂直運動の場合、a は重力による加速度 (−10 m/s2)、s0 は 0 (ボールの初期位置は下) になります。つまり、次の式で表すことができます。

s = v0t -5t2

v = v0 -10t

1 秒で最大高に達する必要があり、最大高での速度は 0 です。そのため、2 つ目の式から初期速度が分かります。

0 = v0 – 10 * 1 => v0 = 10 m/s

そして、ボールの動きは次のように表すことができます。

s = 10t – 5t2

この式を使用するように Update メソッドを変更します。

void Game::Update(DX::StepTimer const& timer)
{
	// シーンの回転
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
	auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
	m_translation = 10*totalTime - 5 *totalTime*totalTime;
}

これでボールがより自然に上下に動くようになりました。次にサッカーフィールドを追加します。

サッカーフィールドを追加する

サッカーフィールドを追加するには、新しいシーンを作成する必要があります。アセットフォルダーで右クリックして、新しい 3 次元の (3D) シーンを追加し、field.fbx という名前を付けます、ツールボックスから平面を追加し、それを選択して、スケールを X が 107、Z が 60 になるように変更します。また、Texture1 プロパティーにサッカーフィールドのイメージを指定します。ズームツールを使用するか、Z キーを押して縮小できます。

次に、Game.cpp にある CreateDeviceDependentResources にメッシュをロードします。

void Game::CreateDeviceDependentResources()
{
	// コードスニペット

	// シーン・オブジェクトをロードします。
	auto loadMeshTask = Mesh::LoadFromFileAsync(
		m_graphics,
		L"gamelevel.cmo",
		L"",
		L"",
		m_meshModels)
		.then([this]()
	{
		return Mesh::LoadFromFileAsync(
			m_graphics,
			L"field.cmo",
			L"",
			L"",
			m_meshModels,
			false  // メッシュのベクトルはクリアしません。
			);
	});

	(loadMeshTask).then([this]()
	{
		// シーンのレンダリング準備完了
		m_loadingComplete = true;
	});
}

プログラムを実行すると、ボールと一緒にフィールドが動きます。フィールドが動かないようにするには、Render メソッドを変更します。

// Starter Kit ヘルパーを使用して 1 フレームをレンダリングします。
void Game::Render()
{
	// コードスニペット

	for (UINT i = 0; i < m_meshModels.size(); i++)
	{
		XMMATRIX modelTransform = rotation;

		String^ meshName = ref new String(m_meshModels[i]->Name());
	
		m_graphics.UpdateMiscConstants(m_miscConstants);

		if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
			m_meshModels[i]->Render(m_graphics, modelTransform);
		else
			m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
	}
}

これで、ボールのみに変換が適用され、フィールドは変換なしでレンダリングされます。コードを実行すると、ボールがフィールドで跳ねますが、着地した際にフィールドに埋没してしまいます。この不具合は、フィールドの y 軸を -0.5 に設定することで修正できます。フィールドを選択し、y 軸の変換プロパティーを −0.5 に変更します。アプリを実行すると、図 8 のようにフィールドでボールが跳ねることを確認できます。


図 8. フィールドで跳ねるボール

カメラとボールの位置を設定する

ボールは、フィールドの中央に配置されていますが、このゲームではペナルティー・マークのある場所に配置する必要があります。図 9 のシーンエディターから、ボールの x 軸を変更しなければならないことが分かります。これは、Game.cpp にある Render メソッドで行います。

rotation *= XMMatrixTranslation(63.0, m_translation, 0);

ボールの x 軸を 63 単位移動し、ペナルティー・マークの場所に配置します。


図 9. X 軸 (赤) と Z 軸 (青) が表示されたフィールド

この変更を行うと、ボールが表示されなくなります。これは、カメラが別の方向 (フィールドの中心) を向いているからです。Game.cpp にある CreateWindowSizeDependentResources で、ゴールラインの方向にカメラを再配置する必要があります。

m_graphics.GetCamera().SetViewport((UINT) outputSize.Width, 
	(UINT) outputSize.Height);
m_graphics.GetCamera().SetPosition(XMFLOAT3(25.0f, 10.0f, 0.0f));
m_graphics.GetCamera().SetLookAt(XMFLOAT3(100.0f, 0.0f, 0.0f));
float aspectRatio = outputSize.Width / outputSize.Height;
float fovAngleY = 30.0f * XM_PI / 180.0f;

if (aspectRatio < 1.0f)
{
	// 縦方向ビューまたはスナップビュー
	m_graphics.GetCamera().SetUpVector(XMFLOAT3(1.0f, 0.0f, 0.0f));
	fovAngleY = 120.0f * XM_PI / 180.0f;
}
else
{
	// 横方向ビュー
	m_graphics.GetCamera().SetUpVector(XMFLOAT3(0.0f, 1.0f, 0.0f));
}
m_graphics.GetCamera().SetProjection(fovAngleY, aspectRatio, 1.0f, 100.0f);

センターマークとペナルティー・マークの間に、ゴールラインの方向を向くようにカメラを配置します。変更後のビューは、図 10 のようになります。


図 10. 新しいカメラ位置で再配置されたボール

次に、ゴールを追加します。

ゴールポストを追加する

フィールドにゴールを追加するには、ゴールを含む新しい 3D シーンが必要です。独自にデザインすることも、規定のモデルを利用することもできます。モデルの準備ができたら、アセットフォルダーに追加して、モデルがコンパイルおよび使用されるようにします。

モデルは、Game.cpp にある CreateDeviceDependentResources メソッドでロードします。

auto loadMeshTask = Mesh::LoadFromFileAsync(
	m_graphics,
	L"gamelevel.cmo",
	L"",
	L"",
	m_meshModels)
	.then([this]()
{
	return Mesh::LoadFromFileAsync(
		m_graphics,
		L"field.cmo",
		L"",
		L"",
		m_meshModels,
		false  // メッシュのベクトルはクリアしません。
		);
}).then([this]()
{
	return Mesh::LoadFromFileAsync(
		m_graphics,
		L"soccer_goal.cmo",
		L"",
		L"",
		m_meshModels,
		false  // メッシュのベクトルはクリアしません。
		);
});

ロード後、Game.cpp にある Render メソッドでモデルを配置し、描画します。

auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * 
	XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5, 0);

for (UINT i = 0; i < m_meshModels.size(); i++)
{
	XMMATRIX modelTransform = rotation;

	String^ meshName = ref new String(m_meshModels[i]->Name());

	m_graphics.UpdateMiscConstants(m_miscConstants);

	if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
		m_meshModels[i]->Render(m_graphics, modelTransform);
	else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0)
		m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
	else
		m_meshModels[i]->Render(m_graphics, goalTransform);
}

これにより、ゴールに次の 3 つの変換が適用され、レンダリングされます: スケール (オリジナルサイズの 2 倍)、90 度回転、そして、x 軸 85.5 単位、y 軸 -0.5 単位 (前述のフィールド位置の調整に対応するため) の移動。これで、図 11 に示すように、ゴールライン上にフィールド側を向いたゴールが配置されます。これらの変換はこの順番で行うことが重要です。例えば、移動後に回転を適用すると、ゴールは全く別の場所にレンダリングされ、何も表示されなくなります。


図 11. ゴールが配置されたフィールド

ボールをキックする

これですべての要素が揃いましたが、ボールはまだ上下に跳ねているだけです。ここでは、ボールをキックする動作を追加します。そのためには、また物理の知識が必要になります。ボールをキックする動作は、図 12 のように表すことができます。


図 12. ボールをキックする動作の図解

ボールは、初期速度 v0、角度 α でキックされます (物理の授業を忘れてしまった場合は、Angry Birds* をプレイすればこの動作を確認できます)。ボールの動きは、一定の加速度の水平運動 (ここでは空気抵抗と風の影響は無視しています) と前述のような垂直運動の 2 つに分けることができます。水平運動の式:

sX = s0 + v0*cos(α)*t

垂直運動の式:

sY = s0 + v0*sin(α)*t – ½*g*t2

つまり、x 軸と y 軸で動きが生じます。45 度でキックした場合、cos(α) = sin(α) = sqrt(2)/2 なので、v0*cos(α) = v0*sin(α)*t となります。キックしたボールがゴールに入るようにするには、(ゴールラインは 85.5 にあるため) 距離が 86 以上でなければなりません。ボールがゴールに到達するまで 2 秒かかるので、1 つ目 の式にこれらの値を代入すると次のようになります。

86 = 63 + v0 * cos(α) * 2 ≥ v0 * cos(α) = 23/2 = 11.5

これらの値を y 軸の式に代入すると、次のようになります。

sY = 0 + 11.5 * t – 5 * t2

そして、x 軸の式に代入すると、次のようになります。

sX = 63 + 11.5 * t

y 軸の式から、2 次方程式の解によりボールがフィールドに着地するタイミングが分かります。

(−b ± sqrt(b2 − 4*a*c))/2*a ≥ (−11.5 ± sqrt(11.52 – 4 * −5 * 0))/2 * −5 ≥ 0 または 23/10 ≥ 2.3s

これらの式を使って、ボールの変換処理を変更します。まず、Game.h で 3 つの軸の変換値を格納する変数を作成します。

float m_translationX, m_translationY, m_translationZ;

そして、Game.cpp にある Update メソッドに式を追加します。

void Game::Update(DX::StepTimer const& timer)
{
	// シーンの回転
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
	auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
	m_translationX = 63.0 + 11.5 * totalTime;
	m_translationY = 11.5 * totalTime - 5 * totalTime*totalTime;
}

Render メソッドは、次のように新しい変換を使用します。

rotation *= XMMatrixTranslation(m_translationX, m_translationY, 0);

プログラムを実行すると、ボールがゴールの中央に入ります。ボールがほかの方向に飛ぶようにするには、キックに水平角を追加する必要があります。そのためには、z 軸を変換します。

図13 からペナルティー・マークとゴール間の距離は 22.5 で、ゴールポスト間の距離は 14 であることが分かります。つまり、α = atan(7/22.5) または 17 度です。z 軸の変換値を計算することもできますが、より簡単な方法は、ボールがゴールラインに到達する時間とゴールポストに到達する時間が同じになるようにします。つまり、x 軸で 1 単位移動する間に z 軸では 7/22.5 単位移動しなければなりません。そのため、z 軸の式は次のようになります。

sz = 11.5 * t/3.2 ≥ sz = 3.6 * t


図 13. ゴールまでの距離の図解

これはゴールポストに到達するための変換です。速度が遅い場合は、角度が小さくなります。ゴールに到達するためには、速度は −3.6 (左側のポスト) と 3.6 (右側のポスト) の範囲でなければなりません。ボールがゴールに完全に入るようにする場合、最大距離は 6/22.5、速度は 3 から −3 の範囲になります。これらの値を利用して、Update メソッドにある次のコードでキックの角度を設定できます。

void Game::Update(DX::StepTimer const& timer)
{
	// シーンの回転
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
	auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
	m_translationX = 63.0 + 11.5 * totalTime;
	m_translationY = 11.5 * totalTime - 5 * totalTime*totalTime;
	m_translationZ = 3 * totalTime;
}

z 軸の変換値は、Render メソッドで使用されます。


rotation *= XMMatrixTranslation(m_translationX, m_translationY, 
	m_translationZ); 
	…

プログラムを実行すると、図 14 のようになります。


図 14. 角度を付けたキック

ゴールキーパーを追加する

ボールの動きとゴールを追加したら、ボールをキャッチするゴールキーパーを追加します。ここでは、立方体を変形したものをゴールキーパーとします。アセットフォルダーに新しい 3D シーンを追加し、goalkeeper.fbx という名前を付けます。

ツールボックスから立方体を追加し、選択して、スケールを x 軸 0.3、y 軸 1.9、z 軸 1 に設定します。MaterialAmbient プロパティーでを 1、を 0 に設定して赤色に変更します。MaterialSpecularは 1 に、MaterialSpecularPower の赤は 0.2 に変更します。

CreateDeviceDependentResources メソッドで新しいリソースをロードします。

auto loadMeshTask = Mesh::LoadFromFileAsync(
	m_graphics,
	L"gamelevel.cmo",
	L"",
	L"",
	m_meshModels)
	.then([this]()
{
	return Mesh::LoadFromFileAsync(
		m_graphics,
		L"field.cmo",
		L"",
		L"",
		m_meshModels,
		false  // メッシュのベクトルはクリアしません。
		);
}).then([this]()
{
	return Mesh::LoadFromFileAsync(
		m_graphics,
		L"soccer_goal.cmo",
		L"",
		L"",
		m_meshModels,
		false  // メッシュのベクトルはクリアしません。
		);
}).then([this]()
{
	return Mesh::LoadFromFileAsync(
		m_graphics,
		L"goalkeeper.cmo",
		L"",
		L"",
		m_meshModels,
		false  // メッシュのベクトルはクリアしません。
		);
});

次に、ゴールの中央にゴールキーパーを配置し、レンダリングします。これは、Game.cppRender メソッドで行います。

void Game::Render()
{
	// コードスニペット

	auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * 
		XMMatrixRotationY(-XM_PIDIV2)* 
		XMMatrixTranslation(85.5f, -0.5f, 0);
	auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.4f, 0);

	for (UINT i = 0; i < m_meshModels.size(); i++)
	{
		XMMATRIX modelTransform = rotation;

		String^ meshName = ref new String(m_meshModels[i]->Name());

		m_graphics.UpdateMiscConstants(m_miscConstants);

		if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
			m_meshModels[i]->Render(m_graphics, modelTransform);
		else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0)
			m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
		else if (String::CompareOrdinal(meshName, L"Cube_Node") == 0)
			m_meshModels[i]->Render(m_graphics, goalkeeperTransform);
		else
			m_meshModels[i]->Render(m_graphics, goalTransform);
	}
}

これにより、図 15 のように、ゴール中央にゴールキーパーが配置されます (このスクリーンショットではカメラが異なる位置にあります)。


図 15. ゴール中央に配置されたゴールキーパー

次に、ゴールキーパーを左右に動かしてボールをキャッチできるようにします。ユーザーは、左矢印/右矢印キーを使用してキーパーを操作します。

ゴールキーパーが移動可能な範囲は、ゴールポスト内 (z 軸の +7 と −7 の範囲) に制限されます。ゴールキーパーは左右 1 ユニット分の幅があるため、実際には左右に 6 ユニット移動することができます。

キー操作は XAML ページ (DirectXPage.xaml) で検出され、Game クラスにリダイレクトされます。DirectXPage.xamlKeyDown イベントハンドラーを追加します。

<Page
    x:Class="StarterKit.DirectXPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:StarterKit"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" KeyDown="OnKeyDown">

DirectXPage.xaml.cpp のイベントハンドラーは次のようになります。

void DirectXPage::OnKeyDown(Platform::Object^ sender, 
	Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e)
{
	m_main->OnKeyDown(e->Key);
}

m_mainStarterKitMain クラスのインスタンスで、ゲームと FPS シーンをレンダリングします。次のように、StarterKitMain.h でパブリックメソッドを宣言する必要があります。

class StarterKitMain : public DX::IDeviceNotify
{
public:
	StarterKitMain(const std::shared_ptr<DX::DeviceResources>& 
		deviceResources);
	~StarterKitMain();

	// パブリックなメソッドを Game レンダラーに直接渡します。
	Platform::String^ OnHitObject(int x, int y) { 
		return m_sceneRenderer->OnHitObject(x, y); }
	void OnKeyDown(Windows::System::VirtualKey key) { 
		m_sceneRenderer->OnKeyDown(key); }
	…

このメソッドは、キーを Game クラスの OnKeyDown メソッドにリダイレクトします。次のように、Game.hOnKeyDown メソッドを宣言します。

class Game
{
public:
	Game(const std::shared_ptr<DX::DeviceResources>& deviceResources);
	void CreateDeviceDependentResources();
	void CreateWindowSizeDependentResources();
	void ReleaseDeviceDependentResources();
	void Update(DX::StepTimer const& timer);
	void Render();
	void OnKeyDown(Windows::System::VirtualKey key);
	…

このメソッドは、キー操作を処理し、ゴールキーパーを移動します。このメソッドを作成する前に、Game.h でゴールキーパーの位置を格納するプライベート変数を宣言する必要があります。

class Game
{
	// コードスニペット

private:
	// コードスニペット

	float m_goalkeeperPosition;

ゴールキーパーの初期位置は 0 で、ユーザーが矢印キーを押すとこの値がインクリメント/デクリメントされます。この値が 6 よりも大きい場合または小さい場合、ゴールキーパーの位置は変わりません。この処理は、Game.cpp にある OnKeyDown メソッドで行います。

void Game::OnKeyDown(Windows::System::VirtualKey key)
{
	const float MaxGoalkeeperPosition = 6.0;
	const float MinGoalkeeperPosition = -6.0;
	if (key == Windows::System::VirtualKey::Right)
		m_goalkeeperPosition = m_goalkeeperPosition >= MaxGoalkeeperPosition ?
	m_goalkeeperPosition : m_goalkeeperPosition + 0.1f;
	else if (key == Windows::System::VirtualKey::Left)
		m_goalkeeperPosition = m_goalkeeperPosition <= MinGoalkeeperPosition ?
	m_goalkeeperPosition : m_goalkeeperPosition - 0.1f;
}

新しい位置は、ゴールキーパーの動きを計算する Game.cppRender メソッドで使用されます。

auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.40f, m_goalkeeperPosition);

これらの変更を適用後、ゲームを実行すると、矢印キーに応じてゴールキーパーが左右に動くことを確認できます (図 16)。


図 16. ゴールキーパーの動きを追加した後のゲーム

ここまで、ボールは常に動き続けていますが、キックした後に動き出し、ゴールに到達したら停止するように変更します。同様に、ゴールキーパーもボールがキックされる前に動かないようにします。

ボールが動き出すタイミングをゲームに知らせるため、Game.h でプライベート変数 m_isAnimating を宣言します。

class Game
{
public:
	// コードスニペット

private:
	// コードスニペット
	bool m_isAnimating;

この変数は、Game.cpp にある Update メソッドと Render メソッドで使用され、ボールは m_isAnimating が true の場合のみ動きます。

void Game::Update(DX::StepTimer const& timer)
{
	if (m_isAnimating)
	{
		m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
		auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
		m_translationX = 63.0f + 11.5f * totalTime;
		m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
		m_translationZ = 3.0f * totalTime;
	}
}

void Game::Render()
{
	// コードスニペット

	XMMATRIX modelTransform;
	if (m_isAnimating)
	{
		modelTransform = XMMatrixRotationY(m_rotation);
		modelTransform *= XMMatrixTranslation(m_translationX, 
			m_translationY, m_translationZ);
	}
	else
		modelTransform = XMMatrixTranslation(63.0f, 0.0f, 0.0f);
	…

上記のコードでは、modelTransform 変数がループからトップに移動されています。矢印キーは、m_isAnimating が true の場合に OnKeyDown メソッドでのみ処理されるべきです。

void Game::OnKeyDown(Windows::System::VirtualKey key)
{
	const float MaxGoalkeeperPosition = 6.0f;
	
	if (m_isAnimating)
	{
		auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ?
			0.1f : -0.1f;

		m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= 
			MaxGoalkeeperPosition ? m_goalkeeperPosition : 
			m_goalkeeperPosition + goalKeeperVelocity;
	}
}

次に、ボールをキックします。ボールは、ユーザーがスペースバーを押すとキックされます。Game.h で新しいプライベート変数 m_isKick を宣言します。

class Game
{
public:
	// コードスニペット

private:
	// コードスニペット
	bool m_isKick;

Game.cpp にある OnKeyDown メソッドでこの変数を true に設定します。

void Game::OnKeyDown(Windows::System::VirtualKey key)
{
	const float MaxGoalkeeperPosition = 6.0f;
	
	if (m_isAnimating)
	{
		auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ?
			0.1f : -0.1f;

		m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= 
			MaxGoalkeeperPosition ? m_goalkeeperPosition : 
			m_goalkeeperPosition + goalKeeperVelocity;
	}
	else if (key == Windows::System::VirtualKey::Space)
		m_isKick = true;
}

m_isKick が true の場合、Update メソッドでアニメーションを開始します。

void Game::Update(DX::StepTimer const& timer)
{
	if (m_isKick)
	{
		m_startTime = static_cast<float>(timer.GetTotalSeconds());
		m_isAnimating = true;
		m_isKick = false;
	}
	if (m_isAnimating)
	{
		auto totalTime = static_cast<float>(timer.GetTotalSeconds()) - 
			m_startTime;
		m_rotation = totalTime * 0.5f;
		m_translationX = 63.0f + 11.5f * totalTime;
		m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
		m_translationZ = 3.0f * totalTime;
		if (totalTime > 2.3f)
			ResetGame();
	}
}

キックしたときの時間は m_startTime 変数 (Game.h でプライベート変数として宣言) に格納され、ボールの移動時間を計算するのに使用されます。ボールの移動時間が 2.3 秒を超えると、ゲームはリセットされます (ボールはすでにゴールに到達しているはずです)。Game.hResetGame メソッドをプライベートとして宣言します。

void Game::ResetGame()
{
	m_isAnimating = false;
	m_goalkeeperPosition = 0;
}

このメソッドは m_isAnimating を false に設定し、ゴールキーパーの位置をリセットします。ボールの位置はリセットする必要がありません。m_isAnimating が false の場合、ペナルティー・マークの場所に描画されるからです。キックの角度も変更します。このコードは、右側のポストの近くにボールが入るようにします。

m_translationZ = 3.0f * totalTime;

ボールがどこに入るかユーザーが予測できないように、この値を変更してランダムにする必要があります。Game.h でプライベート変数 m_ballAngle を宣言し、Update メソッドでボールがキックされたらこの変数を初期化します。

void Game::Update(DX::StepTimer const& timer)
{
	if (m_isKick)
	{
		m_startTime = static_cast<float>(timer.GetTotalSeconds());
		m_isAnimating = true;
		m_isKick = false;	
		m_ballAngle = (static_cast <float> (rand()) /
			static_cast <float> (RAND_MAX) -0.5f) * 6.0f;
	}
…

Rand()/RAND_MAX は 0 から 1 の範囲の値になります。この値から 0.5 を引き (−0.5 から 0.5 の範囲になり)、6 を掛けるので、最終的な角度は −3 から 3 の範囲になります。ゲームごとに異なるシーケンスを使用するには、CreateDeviceDependentResources メソッドで srand を呼び出して乱数ジェネレーターを初期化する必要があります。

void Game::CreateDeviceDependentResources()
{
	srand(static_cast <unsigned int> (time(0)));
…

time 関数を呼び出すには、ctime をインクルードする必要があります。次のように、Update メソッドで m_ballAngle を用いて、新しい角度を適用します。

m_translationZ = m_ballAngle * totalTime;

ここまででほとんどのコーディングが終わりましたが、まだゴールキーパーがボールをキャッチしたか、ゴールが決まったかを確認する作業が残っています。これは、ボールがゴールラインに到達した際に、ボールがゴールキーパーと交差したかどうかチェックすることで簡単に確認できます。もちろん、より複雑な方法を使用することもできますが、ここではこれで十分です。すべての計算は Update メソッドで行われます。

void Game::Update(DX::StepTimer const& timer)
{
	if (m_isKick)
	{
		m_startTime = static_cast<float>(timer.GetTotalSeconds());
		m_isAnimating = true;
		m_isKick = false;
		m_isGoal = m_isCaught = false;
		m_ballAngle = (static_cast <float> (rand()) /
			static_cast <float> (RAND_MAX) -0.5f) * 6.0f;
	}
	if (m_isAnimating)
	{
		auto totalTime = static_cast<float>(timer.GetTotalSeconds()) - 
			m_startTime;
		m_rotation = totalTime * 0.5f;
		if (!m_isCaught)
		{
			// ボールはゴールに向かって移動中
			m_translationX = 63.0f + 11.5f * totalTime;
			m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
			m_translationZ = m_ballAngle * totalTime;
		}
		else
		{
			// ボールがキャッチされた場合は、ゴールキーパーの中央に配置します。
			m_translationX = 83.35f;
			m_translationY = 1.8f;
			m_translationZ = m_goalkeeperPosition;
		}
		if (!m_isGoal && !m_isCaught && m_translationX >= 85.5f)
		{
			// ボールがゴールラインを越えた場合 - ゴールしたか、
			// キャッチされたかを確認します。
			auto ballMin = m_translationZ - 0.5f + 7.0f;
			auto ballMax = m_translationZ + 0.5f + 7.0f;
			auto goalkeeperMin = m_goalkeeperPosition - 1.0f + 7.0f;
			auto goalkeeperMax = m_goalkeeperPosition + 1.0f + 7.0f;
			m_isGoal = (goalkeeperMax < ballMin || goalkeeperMin > ballMax);
			m_isCaught = !m_isGoal;
		}

		if (totalTime > 2.3f)
			ResetGame();
	}
}

Game.h で次の 2 つのプライベート変数を宣言します: m_isGoalm_IsCaught。これらの変数により、ゴールが決まったか、ゴールキーパーがキャッチしたかを判断します。どちらも false の場合、ボールはまだ移動中です。ボールがゴールラインに到達すると、プログラムはボールとゴールキーパーの境界を計算し、それらがオーバーラップしているかどうか判断します。上記のコードでは、各境界に 7.0f が追加されています。境界は正数にも負数にもなる可能性があるため、7.0f を足すことで、すべての値が正数になるようにし、オーバーラップの計算を簡単にしています。ボールがキャッチされると、ボールはゴールキーパーの中央に配置されます。m_isGoalm_IsCaught は、キック時にリセットされます。次に、ゲームにスコアボードを追加します。

スコアボードを追加する

DirectX* ゲームでは、Direct2D を使用してスコアをレンダリングできますが、Windows* 8 ゲームの開発では XAML を利用できます。ゲームで XAML 要素をオーバーラップさせ、XAML 要素とゲームロジック間のブリッジを作成します。この方法は、要素の位置、レンダリング、更新ループの処理が不要なため、情報の表示とユーザー操作をより簡単に行うことができます。

Starter Kit には XAML スコアボードが含まれています (オブジェクトのヒット数のカウントに使用されているものです)。これを編集してスコアを記録することができます。最初に、DirectXPage.xaml を編集してスコアボードを変更します。

<SwapChainPanel x:Name="swapChainPanel" Tapped="OnTapped" >
	<Border VerticalAlignment="Top" HorizontalAlignment="Center" Padding="10" 
		Background="Black" Opacity="0.7">
		<StackPanel Orientation="Horizontal" >
			<TextBlock x:Name="ScoreUser" Text="0" 
				Style="{StaticResource HudCounter}"/>
			<TextBlock Text="x" Style="{StaticResource HudCounter}"/>
			<TextBlock x:Name="ScoreMachine" Text="0" 
				Style="{StaticResource HudCounter}"/>
		</StackPanel>
	</Border>
</SwapChainPanel>

ついでに、このゲームでは画面下部のアプリバーを使用しないので、これを削除します。すべてのヒットカウンターはすでに削除済みなので、DirectXPage.xaml.cpp にある OnTapped ハンドラーでそれらを参照しているコードを削除します。

void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e)
{
	
}

cpp ページと h ページにある OnPreviousColorPressedOnNextColorPressedChangeObjectColor は、削除したアプリバーボタンで使用されているので、これらも削除できます。

ゲームのスコアを更新するには、Game クラスと XAML ページがやり取りできるようにする必要があります。ゲームスコアは Game クラスで更新され、XAML ページに表示されます。1 つの方法として、Game クラスでイベントを作成することができますが、このアプローチには問題があります。Game クラスにイベントを追加すると、次のコンパイルエラーになります: “WinRT イベント宣言は WinRT クラスで行われなければなりません”。このエラーは、GameWinRT (ref) クラスでないために出力されます。WinRT クラスにするには、イベントをパブリックな ref クラスとして定義し、シールする必要があります。

public ref class Game sealed

クラスを変更して対応することもできますが、ここでは別のアプローチをとってみましょう。新しい WinRT クラスを作成し、Game クラスと XAML ページがやり取りできるようにします。新しいクラスを作成し、ViewModel という名前を付けます。

#pragma once
ref class ViewModel sealed
{
public:
	ViewModel();
};

ViewModel.h で、スコアの更新に必要なイベントとプロパティーを追加します。


#pragma once
namespace StarterKit
{
	ref class ViewModel sealed
	{
	private:
		int m_scoreUser;
		int m_scoreMachine;
	public:
		ViewModel();
		event Windows::Foundation::TypedEventHandler<Object^, 
			Platform::String^>^ PropertyChanged;

		property int ScoreUser
		{
			int get()
			{
				return m_scoreUser;
			}

			void set(int value)
			{
				if (m_scoreUser != value)
				{
					m_scoreUser = value;
					PropertyChanged(this, L"ScoreUser");
				}
			}
		};

		property int ScoreMachine
		{
			int get()
			{
				return m_scoreMachine;
			}

			void set(int value)
			{
				if (m_scoreMachine != value)
				{
					m_scoreMachine = value;
					PropertyChanged(this, L"ScoreMachine");
				}
			}
		};
	};

}

Game.hViewModel 型のプライベート変数を宣言します (Game.hViewModel.h をインクルードする必要があります)。また、この変数のパブリックな get アクセス操作子も宣言すべきです。

class Game
{
public:
	// コードスニペット
	StarterKit::ViewModel^ GetViewModel();
private:
	StarterKit::ViewModel^ m_viewModel;

この変数は、Game.cpp のコンストラクターで初期化されます。


Game::Game(const std::shared_ptr<DX::DeviceResources>& deviceResources) :
	m_loadingComplete(false),m_deviceResources(deviceResources)
{
	CreateDeviceDependentResources();
	CreateWindowSizeDependentResources();
	m_viewModel = ref new ViewModel();
}

アクセス操作子のボディーを以下に示します。

StarterKit::ViewModel^ Game::GetViewModel()
{
	return m_viewModel;
}

現在のキックが終了したら、Game.cpp にある ResetGame で変数を更新します。

void Game::ResetGame()
{
	if (m_isCaught)
		m_viewModel->ScoreUser++;
	if (m_isGoal)
		m_viewModel->ScoreMachine++;
	m_isAnimating = false;
	m_goalkeeperPosition = 0;
}

この 2 つのプロパティーのいずれかが変わると、PropertyChanged イベントが発生し、XAML ページで処理されます。まだ 1 つ問題が残っています。XAML ページは Game (ref クラスでない) に直接アクセスできないので、代わりに StarterKitMain クラスを呼び出します。そのため、StarterKitMain.h にある ViewModel のアクセス操作子を作成する必要があります。

class StarterKitMain : public DX::IDeviceNotify
{
public:
	// コードスニペット
	StarterKit::ViewModel^ GetViewModel() 
		{ return m_sceneRenderer->GetViewModel(); }

これにより、ViewModelPropertyChanged イベントを DirectXPage.xaml.cpp のコンストラクターで処理することができます。

DirectXPage::DirectXPage():
	m_windowVisible(true),
	m_hitCountCube(0),
	m_hitCountCylinder(0),
	m_hitCountCone(0),
	m_hitCountSphere(0),
	m_hitCountTeapot(0),
	m_colorIndex(0)
{
	// コードスニペット

	m_main = std::unique_ptr<StarterKitMain>(new 
		StarterKitMain(m_deviceResources));
	m_main->GetViewModel()->PropertyChanged += ref new 
		TypedEventHandler<Object^, String^>(this, 
		&DirectXPage::OnPropertyChanged);
	m_main->StartRenderLoop();
}

ハンドラーによりスコアを更新します (DirectXPage.xaml.cpp.h で宣言する必要があります):

void StarterKit::DirectXPage::OnPropertyChanged(Platform::Object ^sender, 
	Platform::String ^propertyName)
{
	
		if (propertyName == "ScoreUser")
		{
			auto scoreUser = m_main->GetViewModel()->ScoreUser;
			Dispatcher->RunAsync(CoreDispatcherPriority::Normal, 
				ref new DispatchedHandler([this, scoreUser]()
			{
				ScoreUser->Text = scoreUser.ToString();
			}));
		}
		if (propertyName == "ScoreMachine")
		{
			auto scoreMachine= m_main->GetViewModel()->ScoreMachine;
			Dispatcher->RunAsync(CoreDispatcherPriority::Normal, 
				ref new DispatchedHandler([this, scoreMachine]()
			{
				ScoreMachine->Text = scoreMachine.ToString();
			}));
		}
		
}

これで、ゴールが決まるか、ゴールキーパーがキャッチするたびにスコアが更新されます (図 17)。


図 17. スコア更新機能が追加されたゲーム

ゲームでタッチとセンサーを使用する

ここまででゲームは問題なく動作するようになりましたが、さらに機能を追加することができます。新しい Ultrabook™ デバイスのタッチ入力とセンサーを利用して、ゲームを拡張できます。ボールのキックとゴールキーパーの移動をキーボードで操作する代わりに、スクリーンをタップしてボールをキックし、画面を左右に傾けることでゴールキーバーを移動することが可能です。

画面をタップしてボールをキックするには、DirectXPage.cppOnTapped イベントを使用します。

void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e)
{
	m_main->OnKeyDown(VirtualKey::Space);
}

このコードは OnKeyDown メソッドを呼び出し、スペースキーを引数として渡します。つまり、実際にスペースバーを押した場合と同じです。必要に応じて、タップの位置を取得し、ボールがタップされた場合のみキックするようにコードを変更することもできます。それは皆さんへの宿題としておきましょう。Starter Kit には、シーンのオブジェクトがタップされたことを検出するコードが含まれています。

画面を傾けてゴールキーパーを移動するには、画面のすべての動きを検出する傾斜計を利用します。このセンサーは、xyz 軸の回転に対応したピッチ、ロール、ヨーを返します。このゲームではロールのみ必要です。

傾斜計センサーを利用するには、GetDefault メソッドでインターフェイスを取得する必要があります。そして、次に示すように、Game.cpp にある void Game::CreateDeviceDependentResources で検出間隔を設定します。

void Game::CreateDeviceDependentResources()
{
	m_inclinometer = Windows::Devices::Sensors::Inclinometer::GetDefault();
	if (m_inclinometer != nullptr)
	{
		// すべてのシナリオの検出間隔を設定します。
		uint32 minReportInterval = m_inclinometer->MinimumReportInterval;
		uint32 reportInterval = minReportInterval > 16 ? 
			minReportInterval : 16;
		m_inclinometer->ReportInterval = reportInterval;
	}
...

m_inclinometerGame.h で宣言されたプライベート変数です。Update メソッドでゴールキーパーを再配置します。

void Game::Update(DX::StepTimer const& timer)
{
	// コードスニペット
		SetGoalkeeperPosition();
		if (totalTime > 2.3f)
			ResetGame();
	}
}

SetGoalkeeperPosition は、傾斜計の値に応じてゴールキーパーを再配置します。

void StarterKit::Game::SetGoalkeeperPosition()
{
	
	if (m_isAnimating && m_inclinometer != nullptr)
	{
		Windows::Devices::Sensors::InclinometerReading^ reading = 
			m_inclinometer->GetCurrentReading();
		auto goalkeeperVelocity = reading->RollDegrees / 100.0f;
		if (goalkeeperVelocity > 0.3f)
			goalkeeperVelocity = 0.3f;
		if (goalkeeperVelocity < -0.3f)
			goalkeeperVelocity = -0.3f;
		m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= 6.0f ? 
			m_goalkeeperPosition : m_goalkeeperPosition + 
			goalkeeperVelocity;
	}
}

この変更により、画面を傾けることでゴールキーパーを動かせるようになります。これでゲームは完成です。

パフォーマンスを測定する

開発システムでゲームが問題なく実行できることを確認したら、より性能の低いモバイルデバイスで実行してみるべきです。60 FPS の最先端のグラフィックス・プロセッサーを搭載した高性能の開発ワークステーションと、グラフィックス・カード内蔵のインテル® Atom™ プロセッサー・ベースのデバイスでゲームを実行するのでは全く異なるからです。

どちらのマシンでもゲームが優れたパフォーマンスを発揮できるようにすべきです。パフォーマンスの測定には、Visual Studio* またはインテル® グラフィックス・パフォーマンス・アナライザー (インテル® GPA) に含まれるツールを使用できます。インテル® GPA には、ボトルネックを検出し、ゲームのパフォーマンスを向上するさまざまなグラフィックス・アナライザーが含まれており、ゲームのパフォーマンスをグラフィカルに解析し、ゲームをより高速にスムーズに実行できるように支援します。

まとめ

ここでは、踊るティーポットを基にキーボードとセンサー入力に対応した DirectX* ゲームを開発しました。プログラミング言語の類似性が高まりつつある中、C# 開発者にとって C++/CX を使用することはそう難しくありません。

最大の課題は、3D モデルを理解し、それらを移動したり、慣れ親しんだ方法で配置することです。ここではそのために物理、幾何学、三角法、数学を利用しました。

重要なことは、ゲーム開発は不可能ではないということです。少しの忍耐力と正しいツールにより、優れたパフォーマンスのゲームを開発することができます。

謝辞

この記事についてアドバイスを与え、テクニカルレビューを行ってくれた Roberto Sonnino 氏に感謝します。

イメージの出典

参考資料

著者紹介

Bruno Sonnino
ブラジル在住の Microsoft Most Valuable Professional (MVP) です。開発者、コンサルタント、そして著者でもあります。これまでに、Delphi 関連の書籍 5 冊 (ブラジル Pearson Education 刊、ポルトガル語) に加え、ブラジルとアメリカの雑誌/Web サイトで多数の記事を執筆しています。

インテル® デベロッパー・ゾーンは、クロスプラットフォーム・アプリ開発向けのツールとハウツー情報、プラットフォームとテクノロジーに関する情報、コードサンプル、開発者のイノベーションと成功を支援するため他の開発者によるアドバイスを提供します。IoTAndroid*インテル® RealSense™ テクノロジーWindows* コミュニティーに是非ご参加ください。ツールや開発キットをダウンロードしたり、他の開発者と意見交換したり、ハッカソン、コンテスト、ロードショー、ローカルイベントに参加することができます。

Intel、インテル、Intel ロゴ、Intel Atom は、アメリカ合衆国および / またはその他の国における Intel Corporation の商標です。
* その他の社名、製品名などは、一般に各社の表示、商標または登録商標です。
© 2014 Intel Corporation. 無断での引用、転載を禁じます。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事