次の方法で共有


ユーザー インターフェイスの追加

Note

このトピックは、「DirectX を使った単純なユニバーサル Windows プラットフォーム (UWP) ゲームの作成」チュートリアル シリーズの一部です。 リンク先のトピックでは、このシリーズのコンテキストを説明しています。

ゲームに 3D ビジュアルが装備されたので、次は 2D 要素をいくつか追加して、ゲームでプレーヤーにゲームの状態に関するフィードバックを提供できるようにします。 これを実現するには、3-D グラフィックス パイプラインの出力の上に、単純なメニュー オプションとヘッドアップ ディスプレイ コンポーネントを追加します。

Note

このサンプルの最新ゲーム コードをダウンロードしていない場合は、Direct3D サンプル ゲームのページに移動してください。 このサンプルは、UWP 機能サンプルの大規模なコレクションの一部です。 サンプルをダウンロードする方法については、「Windows 開発用のサンプル アプリケーション」をご覧ください。

目的

Direct2D を使用して、UWP DirectX ゲームに次のような多くのユーザー インターフェイスのグラフィックスと動作を追加します。

ユーザー インターフェイス オーバーレイ

テキスト要素とユーザー インターフェイス要素を DirectX ゲームで表示するにはさまざまな方法がありますが、ここでは、Direct2D を中心に説明します。 また、テキスト要素には DirectWrite を使用します。

Direct2D は、ピクセル ベースのプリミティブと効果の描画に使われる 2D 描画 API セットです。 Direct2D を使って作業を始めるときは、物事をシンプルに保つことをお勧めします。 複雑なレイアウトとインターフェイスの動作には、時間と計画が必要です。 シミュレーション ゲームや戦略ゲームで使われているような複雑なユーザー インターフェイスがゲームで必要な場合は、代わりに XAML の使用を検討してください。

Note

XAML を使って UWP DirectX ゲームのユーザー インターフェイスを開発する方法について詳しくは、サンプル ゲームの拡張に関するページを参照してください。

Direct2D は、HTML や XAML のようにユーザー インターフェイスやレイアウト用に特別に設計されたものではありません。 リスト、ボックス、ボタンのようなユーザー インターフェイス コンポーネントは提供されていません。 また、div、テーブル、グリッドのようなレイアウト コンポーネントも提供されていません。

このサンプル ゲームには、2 つの主要な UI コンポーネントがあります。

  1. スコアとゲーム内コントロールのヘッドアップ ディスプレイ。
  2. ゲーム状態のテキストと、オプション (一時停止情報やレベルの開始オプションなど) を表示するために使用されるオーバーレイ。

ヘッドアップ ディスプレイに Direct2D を使用する

次の図は、サンプルのゲーム内ヘッドアップ ディスプレイを示しています。 シンプルですっきりしているため、プレイヤーは 3D の世界のナビゲートと標的を撃つことに集中できます。 優れたインターフェイスやヘッドアップ ディスプレイは、ゲーム内のイベントを処理して反応するプレーヤーの能力を決して複雑にしてはなりません。

ゲーム オーバーレイのスクリーン ショット

オーバーレイは、次の基本的なプリミティブで構成されます。

  • 右上隅の DirectWrite テキスト。プレイヤーに次を知らせます
    • 成功したヒット数
    • プレーヤーが行ったショットの数
    • レベルの残り時間
    • 現在のレベル番号
  • 十字線を形成するために使用される 2 つの交差する線分
  • ムーブ/ルック コントローラーの境界の下隅にある 2 つの長方形。

オーバーレイのゲーム内ヘッドアップ ディプレイの状態は、GameHud クラスの GameHud::Render メソッドで描画されます。 このメソッド内で、UI を表す Direct2D オーバーレイが、ヒット数、残り時間、レベル番号の変更を反映するように更新されます。

ゲームが初期化されている場合は、TotalHits()TotalShots()TimeRemaining()swprintf_s バッファーに追加し、出力形式を指定します。 これで、DrawText メソッドを使用して描画できます。 現在のレベル インジケーターに対して同じことを行い、未完了のレベルを示すために丸数字 (➀ など) を描画し、特定のレベルが完了したことを示すために黒丸数字 (➊ など) を描画します。

次のコード スニペットでは、次の GameHud::Render メソッドのプロセスについて説明します。

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 変数に格納します。 ここでは、DeviceResources クラスの 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 ..." (続行するにはタップしてください...)
  • なし

Note

これらのメソッドはどちらも、「ゲームの状態を表す」セクションで詳しく説明します。

状態アクション のセクションのテキスト フィールドは、ゲームで起こっていることに応じて調整されます。 次は、これら 6 つの状態のオーバーレイを初期化して描画する方法について説明します。

オーバーレイの初期化と描画

6 つの状態にはいくつかの共通点があり、必要なリソースとメソッドが非常に似通っています。 - それらはすべて、画面の中央にある黒い四角形を背景として使用します。 - 表示されるテキストは、タイトルまたは本文テキストのいずれかです。 - テキストは、Segoe UI フォントを使用して、背面の四角形の上に描画されます。

サンプル ゲームには、オーバーレイを作成するときに機能する 4 つのメソッドがあります。

GameInfoOverlay::GameInfoOverlay

GameInfoOverlay::GameInfoOverlay コンストラクターは、オーバーレイを初期化し、プレーヤーに情報を表示するために使用するビットマップ サーフェスを維持します。 コンストラクターは、渡された ID2D1Device オブジェクトからファクトリを取得し、これを使ってオーバーレイ オブジェクト自体が描画できる ID2D1DeviceContextを作成します。 IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources は、テキストの描画に使用されるブラシを作成するためのメソッドです。 これを行うために、ジオメトリの作成と描画を可能にする ID2D1DeviceContext2 オブジェクトと、インクやグラデーション メッシュレンダリングなどの機能を取得します。 次に、ID2D1SolidColorBrush を使用して一連の色付きブラシを作成し、次の UI 要素を描画します。

  • 四角形の背景の黒いブラシ
  • 状態テキストの白いブラシ
  • アクション テキストのオレンジ色のブラシ

DeviceResources::SetDpi

DeviceResources::SetDpi メソッドは、ウィンドウの 1 インチあたりのドット数を設定します。 このメソッドは、DPI が変更されたときに呼び出され、ゲーム ウィンドウのサイズが変更されたときには再調整が必要です。 DPI を更新した後、このメソッドは DeviceResources::CreateWindowSizeDependentResources も呼び出して、ウィンドウのサイズが変更されるたびに必要なリソースが再作成されるようにします。

GameInfoOverlay::CreateWindowsSizeDependentResources

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
        );
    
  • CreateBitmap を使用して、現在の DPI を考慮に入れて m_levelBitmap という名前のビットマップが作成されます。

  • m_levelBitmap は、ID2D1DeviceContext::SetTarget を使用して 2D レンダリング ターゲットとして設定されます。

  • ビットマップは、ID2D1RenderTarget::Clear を使用してすべてのピクセルを黒くしてクリアされます。

  • ID2D1RenderTarget::BeginDraw は、描画を開始するために呼び出されます。

  • DrawText が呼び出され、対応する ID2D1SolidColorBrush を使用して、該当する四角形内の m_titleStringm_bodyStringm_actionString に格納されているテキストを描画します。

  • ID2D1RenderTarget::EndDraw は、m_levelBitmap でのすべての描画操作を停止するために呼び出されます。

  • 別のビットマップが、フォールバックとして使用する m_tooSmallBitmap という名前の CreateBitmap を使用して作成され、表示構成がゲームに対して小さすぎる場合にのみ表示されます。

  • m_tooSmallBitmapm_levelBitmap で描画するプロセスを繰り返します。今回は、本文に文字列 Paused のみを描画します。

ここで必要なのは、6 つのオーバーレイ状態のテキストを入力するための 6 つのメソッドだけです。

ゲームの状態を表す

このゲームの 6 つのオーバーレイの状態には、それぞれに対応するメソッドが GameInfoOverlay オブジェクトにあります。 これらのメソッドは、オーバーレイのバリエーションを描画して、ゲーム自体に関する明示的な情報をプレーヤーに伝えます。 この通信は、タイトル本文の文字列で表されます。 このサンプルでは、初期化時に、GameInfoOverlay::CreateDeviceDependentResources を使用して既にこの情報用のリソースとレイアウトを構成しているため、オーバーレイ状態に固有の文字列を提供するだけで済みます。

オーバーレイの状態部分は、次のいずれかのメソッドを呼び出すことで設定されます。

ゲームの状態 状態の設定メソッド 状態フィールド
読み込み GameInfoOverlay::SetGameLoading タイトル
読み込み
本文
アクティビティの読み込みを示すために、"." を徐々に出力します。
最初の開始/ハイ スコアの統計 GameInfoOverlay::SetGameStats Title
High Score
Body
Levels Completed #
Total Points #
Total Shots#
レベルの開始 GameInfoOverlay::SetLevelStart タイトル
レベル数
本文
レベル目標の説明。
ゲームの一時停止 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;
    }
}

これで、ゲームにゲームの状態に基づいてテキスト情報をプレーヤーに伝達する方法ができ、ゲーム全体でプレーヤーに表示される内容を切り替える方法ができました。

次のステップ

次のトピックの「コントロールの追加」では、プレイヤーがこのサンプル ゲームを操作する方法と、入力によってゲームの状態がどのように変わるかについて説明します。