Sdílet prostřednictvím


Přidání uživatelského rozhraní

Poznámka:

Toto téma je součástí Vytvoření jednoduché hry pro Univerzální platformu Windows (UPW) pomocí série kurzů DirectX. Téma na tomto odkazu nastaví kontext pro řadu.

Teď, když má naše hra svoje 3D vizuály, je čas soustředit se na přidání některých 2D prvků, aby hra mohla poskytnout zpětnou vazbu o stavu hry hráči. Toho lze dosáhnout přidáním jednoduchých možností nabídek a komponentů head-up displeje nad výstupem 3D grafického systému.

Poznámka:

Pokud jste si nestáhli nejnovější kód hry pro tuto ukázku, přejděte na ukázkovou hru Direct3D. Tato ukázka je součástí velké kolekce ukázek funkcí UPW. Pokyny ke stažení ukázky najdete v tématu Ukázkové aplikace pro vývoj ve Windows.

Účel

Pomocí Direct2D přidejte do hry DirectX pro UPW řadu grafiky a chování uživatelského rozhraní, mezi které patří:

Překrytí uživatelského rozhraní

I když ve hře DirectX existuje mnoho způsobů zobrazení prvků textu a uživatelského rozhraní, zaměříme se na použití Direct2D. Budeme také používat directWrite pro textové prvky.

Direct2D je sada rozhraní API pro 2D kreslení, která slouží k kreslení primitiv a efektů založených na pixelech. Při zahájení práce s Direct2D je nejlepší udržovat věci jednoduché. Komplexní rozložení a chování rozhraní potřebují čas a plánování. Pokud vaše hra vyžaduje komplexní uživatelské rozhraní, jako jsou ty, které se nacházejí v simulaci a strategii hry, zvažte místo toho použití XAML.

Poznámka:

Informace o vývoji uživatelského rozhraní pomocí XAML ve hře DirectX UWP naleznete v tématu Rozšíření ukázkové hry.

Direct2D není speciálně navržený pro uživatelská rozhraní ani rozložení, jako je HTML a XAML. Neposkytuje komponenty uživatelského rozhraní, jako jsou seznamy, pole nebo tlačítka. Neposkytuje také komponenty rozložení, jako jsou divy, tabulky nebo mřížky.

Pro tuto ukázkovou hru máme dvě hlavní komponenty uživatelského rozhraní.

  1. Displej s okamžitými informacemi pro skóre a herní ovládací prvky.
  2. Překryv používaný k zobrazení textu stavu hry a možností, jako jsou informace o pozastavení a možnosti zahájení úrovně.

Použití Direct2D pro displej s průhledným zobrazením

Následující obrázek znázorňuje herní rozhraní pro ukázku. Je to jednoduché a přehledné, což umožňuje hráči soustředit se na navigaci ve 3D světě a střílení cílů. Dobré rozhraní nebo head-up displej nesmí nikdy komplikovat schopnost hráče zpracovávat a reagovat na události ve hře.

snímek obrazovky herního overlay

Překryv se skládá z následujících základních primitiv.

  • DirectWrite text v pravém horním rohu, který informuje hráče o
    • Úspěšné zásahy
    • Počet střel, které hráč provedl
    • Zbývající čas na úrovni
    • Číslo aktuální úrovně
  • Dva protínající se segmenty čar používané k vytvoření křížového vlasu
  • Dva obdélníky v dolních rozích pro hranice ovladače pohybu a pohledu

Stav překrytí herního ukazatele v GameHud::Render metodě třídy GameHud. V rámci této metody se aktualizuje překrytí Direct2D, které představuje naše uživatelské rozhraní, aby odráželo změny počtu přístupů, zbývajícího času a počtu úrovní.

Pokud byla hra inicializována, přidáme TotalHits(), TotalShots()a TimeRemaining() do vyrovnávací paměti swprintf_s a určíme formát tisku. Pak ho můžeme nakreslit pomocí metody DrawText. Totéž děláme u indikátoru aktuální úrovně, vykreslení prázdných čísel tak, aby zobrazovala nedokončené úrovně, jako je ➀, a vyplněná čísla, jako je ➊, aby bylo patrné, že se konkrétní úroveň dokončila.

Následující fragment kódu vás provede procesem metody 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
            ...
        }
    }
}

Při dalším rozdělení metody tato část metody GameHud::Render nakreslí obdélníky pro pohyb a střelbu pomocí ID2D1RenderTarget::DrawRectanglea křížky pomocí dvou volání 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
        );
}

V metodě GameHud::Render uložíme logickou velikost herního okna do proměnné windowBounds. To používá metodu třídy DeviceResources.

auto windowBounds = m_deviceResources->GetLogicalSize();

Získání velikosti herního okna je nezbytné pro programování uživatelského rozhraní. Velikost okna je uvedena v měření nazývaném DIPs (nezávislé pixely zařízení), kde dip je definován jako 1/96 palce. Direct2D škáluje jednotky výkresu na skutečné pixely při vykreslování pomocí nastavení bodů na palec (DPI) ve Windows. Podobně, když kreslíte text pomocí DirectWrite, zadáváte DIPs místo bodů pro velikost písma. DIPy jsou vyjádřeny jako čísla s plovoucí desetinnou čárkou. 

Zobrazení informací o stavu hry

Kromě head-up displeje má ukázková hra překrytí, které představuje šest herních stavů. Všechny stavy obsahují velký primitivní černý obdélník s textem pro čtení hráčem. Obdélníky kontroleru pohybu a křížky se nevykreslují, protože nejsou v těchto stavech aktivní.

Překryv se vytvoří pomocí třídy GameInfoOverlay, což nám umožní přepínat zobrazený text tak, aby odpovídal stavu hry.

stav a činnost překrytí

Překryv je rozdělen do dvou částí: Stav a Akce. Oddíl Stav je dále rozdělený do obdélníků Nadpis a Tělo . Sekce Akce obsahuje pouze jeden obdélník. Každý obdélník má jiný účel.

  • titleRectangle obsahuje text nadpisu.
  • bodyRectangle obsahuje základní text.
  • actionRectangle obsahuje text, který informuje hráče o provedení konkrétní akce.

Hra má šest stavů, které lze nastavit. Stav hry vyjádřený pomocí Stav části překrytí. Obdélníky Stav se aktualizují pomocí několika metod odpovídajících následujícím stavům.

  • Nakládka
  • Statistiky počátečního startu a vysokého skóre
  • Začátek úrovně
  • Hra pozastavena
  • Konec hry
  • Vyhráná hra

Část překrytí akce se aktualizuje pomocí metody GameInfoOverlay::SetAction, což umožňuje nastavit text akce na jednu z následujících možností.

  • "Klepněte a přehrát znovu..."
  • Načítání úrovně, prosím čekejte...
  • "Klepnutím pokračujte..."
  • Žádné

Poznámka:

Obě tyto metody budou popsány dále v části Reprezentace stavu hry.

V závislosti na tom, co se děje ve hře, se upraví textová pole Status a Akce v sekci. Pojďme se podívat, jak inicializujeme a nakreslíme překryt těchto šesti stavů.

Inicializace a vykreslování překryvu

šest států Status má několik věcí společných, takže prostředky a metody, které potřebují, jsou velmi podobné. - Všichni používají černý obdélník uprostřed obrazovky jako pozadí. – Zobrazený text je buď Nadpis, nebo Text. – Text používá písmo Segoe UI a je nakresleno nad zadním obdélníkem.

Ukázková hra má čtyři metody, které přicházejí do hry při vytváření překrytí.

GameInfoOverlay::GameInfoOverlay

GameInfoOverlay::GameInfoOverlay konstruktor inicializuje překryv a zachová rastrový povrch, který použijeme k zobrazení informací hráči. Konstruktor získá z ID2D1Device tovární objekt, který používá k vytvoření ID2D1DeviceContext, na který může překryvný objekt kreslit. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources je naše metoda pro vytváření štětců, které se použijí k vykreslení textu. K tomu získáme objekt ID2D1DeviceContext2 , který umožňuje vytváření a kreslení geometrie a funkce, jako je vykreslování rukopisu a přechodové mřížky. Pak vytvoříme řadu barevných štětců pomocí ID2D1SolidColorBrush k vykreslení následujících prvků uživatelského rozhraní.

  • Černý štětec pro obdélníkové pozadí
  • Bílý štětec pro stavový text
  • Oranžový štětec na akční text

DeviceResources::SetDpi

Metoda DeviceResources::SetDpi nastaví body na palec okna. Tato metoda je volána při změně DPI a musí být upravena, k čemuž dojde při změně velikosti herního okna. Po aktualizaci DPI tato metoda také voláDeviceResources::CreateWindowSizeDependentResources k zajištění opětovného vytvoření potřebných prostředků při každé změně velikosti okna.

GameInfoOverlay::CreateWindowsSizeDependentResources

Metoda GameInfoOverlay::CreateWindowsSizeDependentResources je místo, kde probíhá celé naše kreslení. Následuje přehled kroků metody.

  • Jsou vytvořeny tři obdélníky pro oddělení textu uživatelského rozhraní pro text nadpisu, text tělaa text akce.

    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
        );
    
  • Bitmap je vytvořen s názvem m_levelBitmap, s ohledem na aktuální DPI pomocí CreateBitmap.

  • m_levelBitmap je nastaven jako cíl vykreslování 2D pomocí ID2D1DeviceContext::SetTarget.

  • Rastrový obrázek je vymazán tak, že každý pixel je černý pomocí ID2D1RenderTarget::Clear.

  • ID2D1RenderTarget::BeginDraw je volána k zahájení kreslení.

  • DrawText je volán pro kreslení textu uloženého v m_titleString, m_bodyStringa m_actionString v příslušném obdélníku pomocí odpovídající ID2D1SolidColorBrush.

  • ID2D1RenderTarget::EndDraw je volána k zastavení všech operací kreslení na m_levelBitmap.

  • Jiný rastrový obrázek vytvoří se pomocí CreateBitmap nazvaný m_tooSmallBitmap, aby se použil jako záložní, což se ukazuje pouze v případě, že konfigurace zobrazení je pro hru příliš malá.

  • Opakujte proces kreslení na m_levelBitmap pro m_tooSmallBitmap, a tentokrát nakreslete pouze řetězec Paused v těle.

Teď potřebujeme šest metod k vyplnění textu našich šesti překryvných stavů!

Reprezentace stavu hry

Každý ze šesti překryvných stavů ve hře má odpovídající metodu v GameInfoOverlay objektu. Tyto metody nakreslí různé varianty překrytí, aby hráči o samotné hře sdělily explicitní informace. Tato komunikace je reprezentována řetězcem nadpis a text. Vzhledem k tomu, že ukázka už nakonfigurovala prostředky a rozložení pro tyto informace při inicializaci a metodou GameInfoOverlay::CreateDeviceDependentResources, stačí zadat pouze řetězce specifické pro stav překrytí.

Část překryvu Stav je nastavena voláním jedné z následujících metod.

Stav hry Metoda nastavení stavu Pole stavu
Nakládka GameInfoOverlay::SetGameLoading Název
Načítání prostředků
Tělo
Postupně vytiskne '.', čímž naznačuje aktivitu načítání.
Statistiky počátečního startu a vysokého skóre GameInfoOverlay::SetGameStats Nadpis
Vysoké skóre
Body
Úrovně dokončeny #
Celkový počet bodů #
Celkový počet snímků #
Začátek úrovně GameInfoOverlay::SetLevelStart Název
Úroveň #
Tělo
Popis cíle úrovně.
Hra pozastavena GameInfoOverlay::SetPause Nadpis
Hra pozastavena
Tělo
Žádné
Konec hry GameInfoOverlay::SetGameOver Název
Konec hry
Tělo
Dokončené úrovně #
Celkové body #
Celkové střely #
Dokončené úrovně #
Nejvyšší skóre #
Vyhráná hra GameInfoOverlay::SetGameOver Název
Vyhrál(a) jste!
Body
Dokončené úrovně #
Celkový počet bodů #
Celkový počet střel #
Dokončené úrovně #
Vysoké skóre #

Metoda GameInfoOverlay::CreateWindowSizeDependentResources v ukázce deklarovala tři obdélníky, které odpovídají specifickým oblastem překrytí.

S ohledem na tyto oblasti se podíváme na jednu z metod specifických pro stát, GameInfoOverlay::SetGameStats, a zjistíme, jak je nakreslena překryvná vrstva.

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);
    }
}

Pomocí kontextu zařízení Direct2D, který inicializoval objekt GameInfoOverlay, tato metoda vyplní obdélníky názvu a textu černou barvou s použitím štětce pozadí. Nakreslí text pro řetězec "Vysoké skóre" do obdélníku pro nadpis a řetězec obsahující aktualizované informace o stavu hry do základního obdélníku pomocí bílé textové štětce.

Obdélník akce se aktualizuje následným voláním GameInfoOverlay::SetAction z metody na objektu GameMain, který poskytuje informace o stavu hry potřebné pro GameInfoOverlay::SetAction k určení správné zprávy hráči, například "Klepnutím na obrazovku pokračujte".

V metodě GameMain::SetGameInfoOverlay je zvoleno překrytí pro libovolný daný stav:

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;
    }
}

Nyní má hra způsob, jak sdělit hráči textové informace na základě stavu hry, a máme způsob, jak měnit, co se během hry zobrazuje.

Další kroky

V dalším tématu Přidání ovládacích prvků, podíváme se, jak hráč komunikuje s ukázkovou hrou a jak vstup změní stav hry.