注
このトピックは、「DirectX を使った単純なユニバーサル Windows プラットフォーム (UWP) ゲームの作成」チュートリアル シリーズの一部です。 リンク先のトピックでは、このシリーズのコンテキストを説明しています。
ゲームに 3D ビジュアルが装備されたので、次は 2D 要素をいくつか追加して、ゲームでプレーヤーにゲームの状態に関するフィードバックを提供できるようにします。 これを実現するには、3-D グラフィックス パイプラインの出力の上に、単純なメニュー オプションとヘッドアップ ディスプレイ コンポーネントを追加します。
注
このサンプルの最新ゲーム コードをダウンロードしていない場合は、Direct3D サンプル ゲームのページに移動してください。 このサンプルは、UWP 機能サンプルの大規模なコレクションの一部です。 サンプルをダウンロードする方法については、「Windows 開発用のサンプル アプリケーション」をご覧ください。
目的
Direct2D を使用して、UWP DirectX ゲームに次のような多くのユーザー インターフェイスのグラフィックスと動作を追加します。
- ヘッドアップディスプレイには、ムーブルックコントローラーの境界四角形が含まれています。
- ゲームの状態メニュー
ユーザー インターフェイス オーバーレイ
テキスト要素とユーザー インターフェイス要素を DirectX ゲームで表示するにはさまざまな方法がありますが、ここでは、Direct2D を中心に説明します。 また、テキスト要素には DirectWrite を使用します。
Direct2D は、ピクセル ベースのプリミティブと効果の描画に使われる 2D 描画 API セットです。 Direct2D を使って作業を始めるときは、物事をシンプルに保つことをお勧めします。 複雑なレイアウトとインターフェイスの動作には、時間と計画が必要です。 シミュレーション ゲームや戦略ゲームで使われているような複雑なユーザー インターフェイスがゲームで必要な場合は、代わりに XAML の使用を検討してください。
注
XAML を使って UWP DirectX ゲームのユーザー インターフェイスを開発する方法について詳しくは、サンプル ゲームの拡張に関するページを参照してください。
Direct2D は、HTML や XAML のようにユーザー インターフェイスやレイアウト用に特別に設計されたものではありません。 リスト、ボックス、ボタンのようなユーザー インターフェイス コンポーネントは提供されていません。 また、div、テーブル、グリッドのようなレイアウト コンポーネントも提供されていません。
このサンプル ゲームには、2 つの主要な UI コンポーネントがあります。
- スコアとゲーム内コントロールのヘッドアップ ディスプレイ。
- ゲーム状態のテキストと、オプション (一時停止情報やレベルの開始オプションなど) を表示するために使用されるオーバーレイ。
ヘッドアップ ディスプレイに Direct2D を使用する
次の図は、サンプルのゲーム内ヘッドアップ ディスプレイを示しています。 シンプルですっきりしているため、プレイヤーは 3D の世界のナビゲートと標的を撃つことに集中できます。 優れたインターフェイスやヘッドアップ ディスプレイは、ゲーム内のイベントを処理して反応するプレーヤーの能力を決して複雑にしてはなりません。
オーバーレイは、次の基本的なプリミティブで構成されます。
-
右上隅の DirectWrite テキスト。プレイヤーに次を知らせます
- 成功したヒット数
- プレーヤーが行ったショットの数
- レベルの残り時間
- 現在のレベル番号
- 十字線を形成するために使用される 2 つの交差する線分
- ムーブルック コントローラーの境界の下隅にある2つの四角形。
オーバーレイのゲーム内ヘッドアップ ディプレイの状態は、GameHud クラスの GameHud::Render メソッドで描画されます。 このメソッド内で、UI を表す Direct2D オーバーレイが、ヒット数、残り時間、レベル番号の変更を反映するように更新されます。
ゲームが初期化されている場合は、TotalHits()
、TotalShots()
、TimeRemaining()
を swprintf_s バッファーに追加し、出力形式を指定します。 これで、DrawText メソッドを使用して描画できます。 現在のレベル インジケーターに対して同じことを行い、未完了のレベルを示すために丸数字 (➀ など) を描画し、特定のレベルが完了したことを示すために黒丸数字 (➊ など) を描画します。
次のコード スニペットでは、次の GameHud::Render メソッドのプロセスについて説明します。
- **ID2D1RenderTarget::DrawBitmap ** を使用してビットマップを作成する
- D2D1::RectF を使用して UI 領域を四角形に分割する
- DrawText を使用してテキスト要素を作成する
void GameHud::Render(_In_ std::shared_ptr<Simple3DGame> const& game)
{
auto d2dContext = m_deviceResources->GetD2DDeviceContext();
auto windowBounds = m_deviceResources->GetLogicalSize();
if (m_showTitle)
{
d2dContext->DrawBitmap(
m_logoBitmap.get(),
D2D1::RectF(
GameUIConstants::Margin,
GameUIConstants::Margin,
m_logoSize.width + GameUIConstants::Margin,
m_logoSize.height + GameUIConstants::Margin
)
);
d2dContext->DrawTextLayout(
Point2F(m_logoSize.width + 2.0f * GameUIConstants::Margin, GameUIConstants::Margin),
m_titleHeaderLayout.get(),
m_textBrush.get()
);
d2dContext->DrawTextLayout(
Point2F(GameUIConstants::Margin, m_titleBodyVerticalOffset),
m_titleBodyLayout.get(),
m_textBrush.get()
);
}
// Draw text for number of hits, total shots, and time remaining
if (game != nullptr)
{
// This section is only used after the game state has been initialized.
static const int bufferLength = 256;
static wchar_t wsbuffer[bufferLength];
int length = swprintf_s(
wsbuffer,
bufferLength,
L"Hits:\t%10d\nShots:\t%10d\nTime:\t%8.1f",
game->TotalHits(),
game->TotalShots(),
game->TimeRemaining()
);
// Draw the upper right portion of the HUD displaying total hits, shots, and time remaining
d2dContext->DrawText(
wsbuffer,
length,
m_textFormatBody.get(),
D2D1::RectF(
windowBounds.Width - GameUIConstants::HudRightOffset,
GameUIConstants::HudTopOffset,
windowBounds.Width,
GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 3
),
m_textBrush.get()
);
// Using the unicode characters starting at 0x2780 ( ➀ ) for the consecutive levels of the game.
// For completed levels start with 0x278A ( ➊ ) (This is 0x2780 + 10).
uint32_t levelCharacter[6];
for (uint32_t i = 0; i < 6; i++)
{
levelCharacter[i] = 0x2780 + i + ((static_cast<uint32_t>(game->LevelCompleted()) == i) ? 10 : 0);
}
length = swprintf_s(
wsbuffer,
bufferLength,
L"%lc %lc %lc %lc %lc %lc",
levelCharacter[0],
levelCharacter[1],
levelCharacter[2],
levelCharacter[3],
levelCharacter[4],
levelCharacter[5]
);
// Create a new rectangle and draw the current level info text inside
d2dContext->DrawText(
wsbuffer,
length,
m_textFormatBodySymbol.get(),
D2D1::RectF(
windowBounds.Width - GameUIConstants::HudRightOffset,
GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 3 + GameUIConstants::Margin,
windowBounds.Width,
GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 4
),
m_textBrush.get()
);
if (game->IsActivePlay())
{
// Draw the move and fire rectangles
...
// Draw the crosshairs
...
}
}
}
メソッドをさらに細かく分類すると、GameHud::Render メソッドのこの部分は、ID2D1RenderTarget::DrawRectangle を使用してムーブとファイアの四角形を描画し、ID2D1RenderTarget::DrawLine への 2 つの呼び出しを使用して十字線を描画します。
// Check if game is playing
if (game->IsActivePlay())
{
// Draw a rectangle for the touch input for the move control.
d2dContext->DrawRectangle(
D2D1::RectF(
0.0f,
windowBounds.Height - GameUIConstants::TouchRectangleSize,
GameUIConstants::TouchRectangleSize,
windowBounds.Height
),
m_textBrush.get()
);
// Draw a rectangle for the touch input for the fire control.
d2dContext->DrawRectangle(
D2D1::RectF(
windowBounds.Width - GameUIConstants::TouchRectangleSize,
windowBounds.Height - GameUIConstants::TouchRectangleSize,
windowBounds.Width,
windowBounds.Height
),
m_textBrush.get()
);
// Draw the cross hairs
d2dContext->DrawLine(
D2D1::Point2F(windowBounds.Width / 2.0f - GameUIConstants::CrossHairHalfSize,
windowBounds.Height / 2.0f),
D2D1::Point2F(windowBounds.Width / 2.0f + GameUIConstants::CrossHairHalfSize,
windowBounds.Height / 2.0f),
m_textBrush.get(),
3.0f
);
d2dContext->DrawLine(
D2D1::Point2F(windowBounds.Width / 2.0f, windowBounds.Height / 2.0f -
GameUIConstants::CrossHairHalfSize),
D2D1::Point2F(windowBounds.Width / 2.0f, windowBounds.Height / 2.0f +
GameUIConstants::CrossHairHalfSize),
m_textBrush.get(),
3.0f
);
}
GameHud::Render メソッドでは、ゲーム ウィンドウの論理サイズを windowBounds
変数に格納します。 ここでは、GetLogicalSize
クラスの メソッドを使用します。
auto windowBounds = m_deviceResources->GetLogicalSize();
ゲーム ウィンドウのサイズを取得することは、UI プログラミングに不可欠です。 ウィンドウのサイズは、DIP (デバイスに依存しないピクセル数) と呼ばれる単位で示されます。ここでは DIP は 1/96 インチに定義されます。 Direct2D によって、描画の発生時に描画単位が実際のピクセル数に拡大/縮小されます。これには、Windows の DPI (1 インチあたりのドット数) 設定が使用されます。 同様に、DirectWrite を使ってテキストを描画するときは、フォントのサイズにポイント数ではなく DIP を指定します。 DIP は浮動小数点数で表されます。
ゲーム状態情報の表示
ヘッドアップ ディスプレイに加え、サンプル ゲームには、6 つのゲーム状態を表すオーバーレイがあります。 すべての状態には、プレーヤーが読み取るテキストを含む大きな黒い四角形のプリミティブがあります。 ムーブ/ルック コントローラーの四角形と十字線は、これらの状態ではアクティブではないため、描画されません。
オーバーレイは GameInfoOverlay クラスを使用して作成されます。これにより、ゲームの状態に合わせて表示されるテキストを切り替えることができます。
オーバーレイは、状態とアクションの 2 つのセクションに分かれています。 [状態] セクションは、さらに [タイトル] と [本文] の四角形に分かれます。 アクション セクションには四角形が 1 つだけあります。 四角形にはそれぞれ異なる目的があります。
-
titleRectangle
にはタイトル テキストが含まれます。 -
bodyRectangle
には本文テキストが含まれます。 -
actionRectangle
には、特定のアクションを実行するプレーヤーに通知するテキストが含まれます。
このゲームには、設定できる状態が 6 つあります。 ゲームの状態は、オーバーレイの状態部分を使用して伝達されます。 状態の四角形は、次の状態に対応する多数のメソッドを使用して更新されます。
- 読み込み
- 最初の開始/ハイ スコアの統計
- レベルの開始
- ゲームの一時停止
- ゲーム オーバー
- ゲームの勝利
オーバーレイのアクション部分は、GameInfoOverlay::SetAction メソッドを使用して更新され、アクション テキストを次のいずれかに設定できます。
- "Tap to play again..." (一度プレイするにはタップしてください...)
- "Level loading, please wait ..." (読み込んでいます。お待ちください...)
- "Tap to continue ..." (続行するにはタップしてください...)
- なし
注
これらのメソッドはどちらも、「ゲームの状態を表す」セクションで詳しく説明します。
状態とアクション のセクションのテキスト フィールドは、ゲームで起こっていることに応じて調整されます。 次は、これら 6 つの状態のオーバーレイを初期化して描画する方法について説明します。
オーバーレイの初期化と描画
6 つの状態にはいくつかの共通点があり、必要なリソースとメソッドが非常に似通っています。 - それらはすべて、画面の中央にある黒い四角形を背景として使用します。 - 表示されるテキストは、タイトルまたは本文テキストのいずれかです。 - テキストは、Segoe UI フォントを使用して、背面の四角形の上に描画されます。
サンプル ゲームには、オーバーレイを作成するときに機能する 4 つのメソッドがあります。
GameInfoOverlay::GameInfoOverlay
GameInfoOverlay::GameInfoOverlay コンストラクターは、オーバーレイを初期化し、プレーヤーに情報を表示するために使用するビットマップ サーフェスを維持します。 コンストラクターは、渡された ID2D1Device オブジェクトからファクトリを取得し、これを使ってオーバーレイ オブジェクト自体が描画できる ID2D1DeviceContextを作成します。 IDWriteFactory::CreateTextFormat
GameInfoOverlay::デバイスに依存するリソースを作成
GameInfoOverlay::CreateDeviceDependentResources は、テキストの描画に使用されるブラシを作成するためのメソッドです。 これを行うために、ジオメトリの作成と描画を可能にする ID2D1DeviceContext2 オブジェクトと、インクやグラデーション メッシュレンダリングなどの機能を取得します。 次に、ID2D1SolidColorBrush を使用して一連の色付きブラシを作成し、次の UI 要素を描画します。
- 四角形の背景の黒いブラシ
- 状態テキストの白いブラシ
- アクション テキストのオレンジ色のブラシ
DeviceResources::SetDpi
DeviceResources::SetDpi メソッドは、ウィンドウの 1 インチあたりのドット数を設定します。 このメソッドは、DPI が変更されたときに呼び出され、ゲーム ウィンドウのサイズが変更されたときには再調整が必要です。 DPI を更新した後、このメソッドは DeviceResources::CreateWindowSizeDependentResources も呼び出して、ウィンドウのサイズが変更されるたびに必要なリソースが再作成されるようにします。
GameInfoOverlay::CreateWindowsサイズ依存リソース
GameInfoOverlay::CreateWindowsSizeDependentResources メソッドは、すべての描画が行われる場所です。 メソッドのステップの概要を次に示します。
タイトル、本文、アクション のテキストの UI テキストを区切るために、3 つの四角形が作成されます。
m_titleRectangle = D2D1::RectF( GameInfoOverlayConstant::SideMargin, GameInfoOverlayConstant::TopMargin, overlaySize.width - GameInfoOverlayConstant::SideMargin, GameInfoOverlayConstant::TopMargin + GameInfoOverlayConstant::TitleHeight ); m_actionRectangle = D2D1::RectF( GameInfoOverlayConstant::SideMargin, overlaySize.height - (GameInfoOverlayConstant::ActionHeight + GameInfoOverlayConstant::BottomMargin), overlaySize.width - GameInfoOverlayConstant::SideMargin, overlaySize.height - GameInfoOverlayConstant::BottomMargin ); m_bodyRectangle = D2D1::RectF( GameInfoOverlayConstant::SideMargin, m_titleRectangle.bottom + GameInfoOverlayConstant::Separator, overlaySize.width - GameInfoOverlayConstant::SideMargin, m_actionRectangle.top - GameInfoOverlayConstant::Separator );
m_levelBitmap
を使用して、現在の DPI を考慮に入れて という名前のビットマップが作成されます。m_levelBitmap
は、ID2D1DeviceContext::SetTarget を使用して 2D レンダリング ターゲットとして設定されます。ビットマップは、ID2D1RenderTarget::Clear を使用してすべてのピクセルを黒くしてクリアされます。
ID2D1RenderTarget::BeginDraw は、描画を開始するために呼び出されます。
DrawText が呼び出され、対応する
m_titleString
を使用して、該当する四角形内のm_bodyString
、m_actionString
、 に格納されているテキストを描画します。ID2D1RenderTarget::EndDraw は、
m_levelBitmap
でのすべての描画操作を停止するために呼び出されます。別のビットマップが、フォールバックとして使用する という名前の
m_tooSmallBitmap
を使用して作成され、表示構成がゲームに対して小さすぎる場合にのみ表示されます。m_levelBitmap
のm_tooSmallBitmap
で描画するプロセスを繰り返します。今回は、本文に文字列Paused
のみを描画します。
ここで必要なのは、6 つのオーバーレイ状態のテキストを入力するための 6 つのメソッドだけです。
ゲームの状態を表す
このゲームの 6 つのオーバーレイの状態には、それぞれに対応するメソッドが GameInfoOverlay オブジェクトにあります。 これらのメソッドは、オーバーレイのバリエーションを描画して、ゲーム自体に関する明示的な情報をプレーヤーに伝えます。 この通信は、タイトルと本文の文字列で表されます。 このサンプルでは、初期化時に、GameInfoOverlay::CreateDeviceDependentResources を使用して既にこの情報用のリソースとレイアウトを構成しているため、オーバーレイ状態に固有の文字列を提供するだけで済みます。
オーバーレイの状態部分は、次のいずれかのメソッドを呼び出すことで設定されます。
ゲームの状態 | 状態の設定メソッド | 状態フィールド |
---|---|---|
読み込み | GameInfoOverlay::SetGameLoading |
タイトル 読み込み 本文 アクティビティの読み込みを示すために、"." を徐々に出力します。 |
最初の開始/ハイ スコアの統計 | GameInfoOverlay::SetGameStats |
Title High Score Body Levels Completed # Total Points # Total Shots# |
レベルの開始 | ゲーム情報オーバーレイ::レベル開始設定 |
タイトル レベル数 本文 レベル目標の説明。 |
ゲームの一時停止 | GameInfoOverlay::SetPause |
タイトル 一時停止されたゲーム 本文 なし |
ゲーム オーバー | GameInfoOverlay::SetGameOver |
Title Game Over Body Levels Completed # Total Points # Total Shots # Levels Completed # High Score# |
ゲームの勝利 | GameInfoOverlay::SetGameOver |
タイトル WON! Body Levels Completed # Total Points # Total Shots # Levels Completed # High Score# |
このサンプルでは、GameInfoOverlay::CreateWindowSizeDependentResources メソッドを使用して、オーバーレイの特定の領域に対応する 3 つの四角形の領域を宣言しました。
これらの領域を念頭に置いて、状態固有のメソッド の 1 つである GameInfoOverlay::SetGameStats を考察し、オーバーレイの描画方法を見てみましょう。
void GameInfoOverlay::SetGameStats(int maxLevel, int hitCount, int shotCount)
{
int length;
auto d2dContext = m_deviceResources->GetD2DDeviceContext();
d2dContext->SetTarget(m_levelBitmap.get());
d2dContext->BeginDraw();
d2dContext->SetTransform(D2D1::Matrix3x2F::Identity());
d2dContext->FillRectangle(&m_titleRectangle, m_backgroundBrush.get());
d2dContext->FillRectangle(&m_bodyRectangle, m_backgroundBrush.get());
m_titleString = L"High Score";
d2dContext->DrawText(
m_titleString.c_str(),
m_titleString.size(),
m_textFormatTitle.get(),
m_titleRectangle,
m_textBrush.get()
);
length = swprintf_s(
wsbuffer,
bufferLength,
L"Levels Completed %d\nTotal Points %d\nTotal Shots %d",
maxLevel,
hitCount,
shotCount
);
m_bodyString = std::wstring(wsbuffer, length);
d2dContext->DrawText(
m_bodyString.c_str(),
m_bodyString.size(),
m_textFormatBody.get(),
m_bodyRectangle,
m_textBrush.get()
);
// We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
// is lost. It will be handled during the next call to Present.
HRESULT hr = d2dContext->EndDraw();
if (hr != D2DERR_RECREATE_TARGET)
{
// The D2DERR_RECREATE_TARGET indicates there has been a problem with the underlying
// D3D device. All subsequent rendering will be ignored until the device is recreated.
// This error will be propagated and the appropriate D3D error will be returned from the
// swapchain->Present(...) call. At that point, the sample will recreate the device
// and all associated resources. As a result, the D2DERR_RECREATE_TARGET doesn't
// need to be handled here.
winrt::check_hresult(hr);
}
}
このメソッドは、GameInfoOverlay オブジェクトが初期化した Direct2D デバイス コンテキストと、背景ブラシを使って、タイトルと本文の四角形を黒で塗りつぶします。 "High Score" という文字列のテキストをタイトルの四角形に、ゲームの状態情報の更新を含む文字列を本文の四角形に、白のテキスト ブラシを使用して描画します。
アクションの四角形は、GameMain オブジェクトのメソッドから GameInfoOverlay::SetAction を後で呼び出すと更新されます。このオブジェクトは、GameInfoOverlay::SetAction がプレイヤーに対する適切なメッセージ ("続行するにはタップしてください" など) を判断するために必要なゲームの状態情報を提供します。
特定の状態用のオーバーレイは、GameMain::SetGameInfoOverlay メソッドで次のように選択されます。
void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
m_gameInfoOverlayState = state;
switch (state)
{
case GameInfoOverlayState::Loading:
m_uiControl->SetGameLoading(m_loadingCount);
break;
case GameInfoOverlayState::GameStats:
m_uiControl->SetGameStats(
m_game->HighScore().levelCompleted + 1,
m_game->HighScore().totalHits,
m_game->HighScore().totalShots
);
break;
case GameInfoOverlayState::LevelStart:
m_uiControl->SetLevelStart(
m_game->LevelCompleted() + 1,
m_game->CurrentLevel()->Objective(),
m_game->CurrentLevel()->TimeLimit(),
m_game->BonusTime()
);
break;
case GameInfoOverlayState::GameOverCompleted:
m_uiControl->SetGameOver(
true,
m_game->LevelCompleted() + 1,
m_game->TotalHits(),
m_game->TotalShots(),
m_game->HighScore().totalHits
);
break;
case GameInfoOverlayState::GameOverExpired:
m_uiControl->SetGameOver(
false,
m_game->LevelCompleted(),
m_game->TotalHits(),
m_game->TotalShots(),
m_game->HighScore().totalHits
);
break;
case GameInfoOverlayState::Pause:
m_uiControl->SetPause(
m_game->LevelCompleted() + 1,
m_game->TotalHits(),
m_game->TotalShots(),
m_game->TimeRemaining()
);
break;
}
}
これで、ゲームにゲームの状態に基づいてテキスト情報をプレーヤーに伝達する方法ができ、ゲーム全体でプレーヤーに表示される内容を切り替える方法ができました。
次のステップ
次のトピックの「コントロールの追加」では、プレイヤーがこのサンプル ゲームを操作する方法と、入力によってゲームの状態がどのように変わるかについて説明します。