新增使用者介面

注意

本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。

現在遊戲有了 3D 視覺效果,接著可以著重於新增 2D 元素,讓遊戲向玩家提供有關遊戲狀態的回饋。 在 3D 圖形管線輸出結果上,新增簡單的功能表選項和平視顯示元件,即可做到這點。

注意

如果您尚未下載此範例的最新遊戲程式碼,請至 Direct3D 範例遊戲頁面下載。 此範例屬於大型 UWP 功能範例集。 如需範例下載的相關指示,請參閱<適用 Windows 開發的範例應用程式>。

目標

使用 Direct2D,將許多使用者介面圖形和行為新增至 UWP DirectX 遊戲,包括:

使用者介面重疊

雖然在 DirectX 遊戲中有許多方法可顯示文字和使用者介面元素,但我們會著重於使用 Direct2D。 我們也會針對文字元素使用 DirectWrite

Direct2D 是 2D 繪圖 API,用來繪製以像素為基礎的基本類型和效果。 從 Direct2D 開始著手時,建議保持簡單。 複雜的版面配置和介面行為需要時間和規劃。 如果遊戲需要複雜的使用者介面 (例如:模擬和策略遊戲中的介面),請考慮改用 XAML。

注意

如需在 UWP DirectX 遊戲中使用 XAML 開發使用者介面的相關資訊,請參閱<延伸範例遊戲>。

Direct2D 不是專為使用者介面或版面配置 (如 HTML 和 XAML) 所設計, 不提供清單、方塊或按鈕等使用者介面元件, 也不提供 divs、資料表或方格等版面配置元件。

此範例遊戲有兩個主要的 UI 元件。

  1. 分數和遊戲內控制項的平視顯示。
  2. 顯示遊戲狀態文字和選項的重疊項目,例如:暫停資訊和層級開始選項。

使用 Direct2D 進行平視顯示

下圖示範如何在遊戲內呈現平視顯示。 畫面簡單整齊,可讓玩家專注於瀏覽 3D 世界和射擊目標。 理想的介面或平視顯示設計絕不會讓玩家在處理和回應遊戲中事件的過程複雜化。

遊戲疊加層的螢幕截圖

重疊是由下列基本的原始項目組成。

  • DirectWrite 文字顯示於右上角,通知玩家以下情況:
    • 成功擊中
    • 擊中玩家的次數
    • 該關卡剩餘時間
    • 目前的關卡等級
  • 兩個交集線段,用來形成十字準線
  • 移動視角控制器界限最下方的兩個矩形。

遊戲內的重疊平視顯示狀態是在 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 呼叫繪製十字準線。

// 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) 設定,將繪圖單位縮放為實際像素。 同樣地,當您使用 DirectWrite 繪製文字時,請指定 DIP,而非字型點數大小。 DIP 以浮點數表示。 

顯示遊戲狀態資訊

除了平視顯示,範例遊戲還有代表六種遊戲狀態的重疊。 所有狀態都附有大型的黑色矩形基本物件 (含有供玩家閱讀的文字)。 移動視角控制器矩形和十字準線在這些狀態中沒有作用,因此不繪製。

重疊是使用 GameInfoOverlay 類別建立,可讓我們根據遊戲狀態切換出要顯示的文字。

覆蓋的狀態和操作

重疊分成兩個區段:StatusAction。 狀態部分進一步分為標題正文矩形。 Action 區段只有一個矩形。 每個矩形各有不同的用途。

  • titleRectangle 包含標題文字。
  • bodyRectangle 包含內文文字。
  • actionRectangle 包含通知玩家採取特定動作的文字。

遊戲有六種狀態可設定。 使用重疊的 Status 部分傳達遊戲狀態。 Status 矩形會使用多種對應下列狀態的方法進行更新:

  • 正在載入
  • 初始開始/高分統計
  • 關卡開始
  • 遊戲暫停
  • 遊戲結束
  • 遊戲獲勝

重疊的 Action 部分使用 GameInfoOverlay::SetAction 方法更新,允許將動作文字設為下列其中一項。

  • 「點選以再玩一次...」
  • 「關卡載入中,請稍候...」
  • 「點選以繼續...」

注意

我們會在<表示遊戲狀態>一節進一步說明這兩種方法。

StatusAction 區段的文字欄位會隨遊戲中發生的事情而調整。 我們來看看如何初始化和繪製這六種狀態的重疊。

初始化和繪製重疊

六種 Status 狀態有一些共通點,因此需要的資源和方法非常類似: - 全都在畫面中央使用黑色矩形作為背景。 - 顯示的文字為 TitleBody 文字。 - 文字使用 Segoe UI 字型,並繪製於背景矩形上。

範例遊戲提供的四種方法會在建立重疊時生效。

GameInfoOverlay::GameInfoOverlay

GameInfoOverlay::GameInfoOverlay 建構函式會初始化重疊,並維持用來向玩家顯示資訊的點陣圖表面。 建構函式從傳遞而來的 ID2D1Device 物件取得處理站,用來建立重疊物件本身可繪製的 ID2D1DeviceContextIDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources 是用來建立文字繪製筆刷的方法。 若要執行此工作,請取得 ID2D1DeviceContext2 物件,以啟用幾何建立和繪製作業,以及筆跡和漸層網格轉譯等功能。 然後,我們使用 ID2D1SolidColorBrush 建立一系列彩色畫筆來繪製以下 UI 元素。

  • 適用矩形背景的黑色筆刷
  • 適用狀態文字的白色筆刷
  • 適用動作文字的橙色筆刷

DeviceResources::SetDpi

DeviceResources::SetDpi 方法會設定視窗的每英吋點數。 DPI 變更且需要重新調整 (發生於遊戲視窗調整大小) 時,系統會呼叫此方法。 更新 DPI 之後,此方法也會呼叫 DeviceResources::CreateWindowSizeDependentResources,確保每次視窗調整大小時都會重新建立必要的資源。

GameInfoOverlay::CreateWindowsSizeDependentResources

GameInfoOverlay::CreateWindowsSizeDependentResources 方法即所有繪製作業的執行位置。 以下概述該方法的步驟。

  • 系統會建立三個矩形,區分 TitleBodyAction 文字的 UI 文字元素。

    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 的點陣圖,且使用CreateBitmap 將目前的 DPI 納入考量。

  • 使用ID2D1DeviceContext::SetTargetm_levelBitmap 設為 2D 轉譯器目標。

  • 使用 ID2D1RenderTarget::Clear 清除點陣圖,其中每個像素都變成黑色。

  • 呼叫 ID2D1RenderTarget::BeginDraw 以初始化繪製。

  • 喚醒 DrawText 來繪製儲存在m_titleString中的文字,m_bodyString並使用相應的 m_actionStringID2D1SolidColorBrush 在適當的矩形中。

  • 呼叫 ID2D1RenderTarget::EndDraw 停止 m_levelBitmap 上的所有繪製作業。

  • 另一個點陣圖是使用 CreateBitmap (稱為 m_tooSmallBitmap) 所建立,當成後援使用,只在顯示器組態對遊戲來說太小時才會顯示。

  • 針對 m_tooSmallBitmapm_levelBitmap 重複執行繪製程序,這次只在內文中繪製 Paused

現在我們需要使用六種方法,填滿六個重疊狀態的文字!

表示遊戲狀態

遊戲中的六個重疊狀態在 GameInfoOverlay 物件中,各有一個對應的方法。 這些方法可繪製重疊的變化,以向玩家傳達有關遊戲的明確資訊。 通訊是以 TitleBody 字串表示。 由於範例在初始化時已設定此資訊的資源和配置,且若使用 GameInfoOverlay::CreateDeviceDependentResources 方法,則只需要提供重疊狀態特定的字串即可。

重疊的 Status 部分已使用呼叫設為下列方法之一。

遊戲狀態 狀態設定方法 狀態欄位
正在載入 GameInfoOverlay::SetGameLoading Title
載入資源
Body
列印多個「.」以表示載入活動。
初始開始/高分統計 GameInfoOverlay::SetGameStats Title
高分
Body
關卡完成 #
總分 #
總射擊數 #
關卡開始 GameInfoOverlay::SetLevelStart Title
關卡 #
Body
關卡目標描述。
遊戲暫停 GameInfoOverlay::SetPause Title
遊戲暫停
Body
遊戲結束 GameInfoOverlay::SetGameOver Title
遊戲結束
Body
關卡完成 #
總分 #
總射擊數 #
關卡完成 #
高分 #
遊戲獲勝 GameInfoOverlay::SetGameOver Title
恭喜獲勝!
Body
關卡完成 #
總分 #
總射擊數 #
關卡完成 #
高分 #

使用 GameInfoOverlay::CreateWindowSizeDependentResources 方法,範例會宣告三個矩形區域,其對應重疊的特定區域。

請考量這些區域,然後來看看其中一個狀態特定的方法 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 裝置內容,以背景筆刷在標題和內文矩形中填滿黑色。 該方法使用白色文字筆刷,在標題矩形繪製「高分」字串文字,並在內文矩形繪製包含遊戲更新狀態資訊的字串。

後續對 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;
    }
}

現在,遊戲已可根據遊戲狀態向玩家傳達文字資訊,且可在整個遊戲中切換顯示內容。

下一步

下一個主題<新增控制項>將說明玩家如何與範例遊戲互動,以及輸入如何變更遊戲狀態。