Dela via


Lägga till ett användargränssnitt

Anmärkning

Det här avsnittet är en del av Skapa ett enkelt UWP-spel (Universal Windows Platform) med DirectX självstudieserie. Ämnet på länken anger kontexten för serien.

Nu när vårt spel har sina 3D-visuella objekt på plats är det dags att fokusera på att lägga till några 2D-element så att spelet kan ge feedback om speltillståndet till spelaren. Detta kan åstadkommas genom att lägga till enkla menyalternativ och heads-up-visningskomponenter ovanpå 3D-grafikpipelinens utdata.

Anmärkning

Om du inte har laddat ned den senaste spelkoden för det här exemplet går du till Direct3D-exempelspel. Det här exemplet är en del av en stor samling UWP-funktionsexempel. Anvisningar om hur du laddar ned exemplet finns i Exempelprogram för Windows-utveckling.

Mål

Med Direct2D lägger du till ett antal användargränssnittsgrafik och beteenden i vårt UWP DirectX-spel, inklusive:

Överlägget för användargränssnittet

Det finns många sätt att visa text- och användargränssnittselement i ett DirectX-spel, men vi kommer att fokusera på att använda Direct2D-. Vi kommer också att använda DirectWrite- för textelementen.

Direct2D är en uppsättning 2D-ritnings-API:er som används för att rita pixelbaserade primitiver och effekter. När du börjar med Direct2D är det bäst att hålla det enkelt. Komplexa layouter och gränssnittsbeteenden behöver tid och planering. Om ditt spel kräver ett komplext användargränssnitt, som de som finns i simulerings- och strategispel, bör du överväga att använda XAML i stället.

Anmärkning

Information om hur du utvecklar ett användargränssnitt med XAML i ett UWP DirectX-spel finns i Utöka exempelspelet.

Direct2D är inte särskilt utformat för användargränssnitt eller layouter som HTML och XAML. Den tillhandahåller inte komponenter i användargränssnittet som listor, rutor eller knappar. Den tillhandahåller inte heller layoutkomponenter som divs, tabeller eller rutnät.

För det här exempelspelet har vi två viktiga gränssnittskomponenter.

  1. En heads-up-skärm för poängen och kontrollerna i spelet.
  2. Ett överlägg som används för att visa speltillståndstext och alternativ som pausinformation och startalternativ för nivå.

Använda Direct2D för en heads-up-skärm

Följande bild visar heads-up-skärmen i spelet för exemplet. Det är enkelt och stilrent, så att spelaren kan fokusera på att navigera i 3D-världen och skjuta mål. Ett bra gränssnitt eller heads-up-skärm får aldrig komplicera spelarens förmåga att bearbeta och reagera på händelserna i spelet.

en skärmdump av spelets överlägg

Överlägget består av följande grundläggande primitiver.

  • DirectWrite text i det övre högra hörnet som informerar spelaren om
    • Lyckade träffar
    • Antal skott som spelaren har gjort
    • Återstående tid på nivån
    • Aktuellt nivånummer
  • Två korsande linjesegment som används för att bilda ett hårkors
  • Två rektanglar i de nedre hörnen för kontrollerns flytt-se gränser.

Visningstillståndet för överlägget i spelet visas i GameHud::Render-metoden i klassen GameHud. I den här metoden uppdateras Direct2D-överlägget som representerar vårt användargränssnitt för att återspegla ändringarna i antalet träffar, återstående tid och nivånummer.

Om spelet har initierats lägger vi till TotalHits(), TotalShots()och TimeRemaining() till en swprintf_s buffert och anger utskriftsformatet. Vi kan sedan rita den med hjälp av metoden DrawText. Vi gör samma sak för den aktuella nivåindikatorn och ritar tomma tal för att visa okompletterade nivåer som ➀ och fyllda tal som ➊ för att visa att den specifika nivån slutfördes.

Följande kodfragment går igenom GameHud::Render metodens process för

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

Genom att bryta ned metoden ytterligare ritar den här delen av metoden GameHud::Render vår flytt- och brandrektanglar med ID2D1RenderTarget::D rawRectangleoch hårkors med hjälp av två anrop till ID2D1RenderTarget::D rawLine.

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

I metoden GameHud::Render lagrar vi spelfönstrets logiska storlek i variabeln windowBounds. Detta använder GetLogicalSize-metoden för klassen DeviceResources.

auto windowBounds = m_deviceResources->GetLogicalSize();

Att få storleken på spelfönstret är viktigt för UI-programmering. Storleken på fönstret anges i ett mått som kallas DIPs (enhetsoberoende bildpunkter), där en DIP definieras som 1/96 tum. Direct2D skalar ritningsenheterna till faktiska pixlar när ritningen utförs, och använder sig då av Windows inställning för punkter per tum (DPI). När du ritar text med DirectWriteanger du på samma sätt DIP:er i stället för punkter för teckensnittets storlek. DIP:er uttrycks som flyttalsnummer. 

Visa speltillståndsinformation

Förutom heads-up-skärmen har exempelspelet ett överlägg som representerar sex speltillstånd. Alla lägen har en stor svart rektangel med text som spelaren kan läsa. Rektanglar och hårkors ritas inte eftersom de inte är aktiva i dessa tillstånd.

Överlägget skapas med hjälp av klassen GameInfoOverlay, så att vi kan växla ut vilken text som visas för att anpassa till spelets tillstånd.

status och åtgärder för överlagring

Överlägget är uppdelat i två avsnitt: Status och Action. Avsnittet Status är ytterligare uppdelat i rektanglar för rubrik och brödtext . Avsnittet Åtgärd har bara en rektangel. Varje rektangel har ett annat syfte.

  • titleRectangle innehåller rubriktexten.
  • bodyRectangle innehåller brödtexten.
  • actionRectangle innehåller texten som informerar spelaren om att vidta en specifik åtgärd.

Spelet har sex tillstånd som kan ställas in. Spelets tillstånd förmedlas genom del Status av överlägget. Rektanglarna Status uppdateras med ett antal metoder som motsvarar följande tillstånd.

  • Laddar
  • Inledande start-/högpoängsstatistik
  • Nivåstart
  • Spelet har pausats
  • Spelet är slut
  • Spelet vanns

Den åtgärden delen av överlägget uppdateras med hjälp av metoden GameInfoOverlay::SetAction, vilket gör att åtgärdstexten kan anges till något av följande.

  • Tryck för att spela upp igen...
  • Laddar nivå, var god vänta ...
  • "Tryck för att fortsätta ..."
  • Ingen

Anmärkning

Båda dessa metoder kommer att diskuteras ytterligare i avsnittet Representerar speltillstånd.

Beroende på vad som händer i spelet justeras textfälten i Status-sektionen och Action-sektionen. Nu ska vi titta på hur vi initierar och ritar överlägget för dessa sex tillstånd.

Initiera och rita överlägget

De sex status stater har några saker gemensamt, vilket gör att resurserna och metoderna de behöver är mycket lika. - De använder alla en svart rektangel i mitten av skärmen som bakgrund. - Den text som visas är antingen Rubrik eller brödtext text. – Texten använder Segoe UI-teckensnittet och ritas ovanpå den bakre rektangeln.

Exempelspelet har fyra metoder som spelar in när du skapar överlägget.

GameInfoOverlay::GameInfoOverlay

GameInfoOverlay::GameInfoOverlay konstruktorn initierar överlägget och bibehåller bitmappsytan som vi använder för att visa information till spelaren. Konstruktorn hämtar en fabrik från ID2D1Enhet-objektet som skickas till den, som används för att skapa en ID2D1DeviceContext som själva överlappningsobjektet kan rita på. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources är vår metod för att skapa penslar som ska användas för att rita texten. För att göra detta hämtar vi ett ID2D1DeviceContext2-objekt som möjliggör skapande och ritning av geometri, plus funktioner som ink- och gradient mesh-återgivning. Sedan skapar vi en serie färgade penslar med ID2D1SolidColorBrush för att rita följande gränssnittselement.

  • Svart pensel för rektangelbakgrunder
  • Vit pensel för statustext
  • Orange pensel för åtgärdstext

DeviceResources::SetDpi

Metoden DeviceResources::SetDpi anger punkterna per tum i fönstret. Den här metoden anropas när DPI ändras och måste justeras, vilket händer när storleken på spelfönstret ändras. När du har uppdaterat DPI:n anropar denna metod ävenDeviceResources::CreateWindowSizeDependentResources för att se till att nödvändiga resurser återskapas varje gång fönstret ändras i storlek.

GameInfoOverlay::CreateWindowsSizeDependentResources

Metoden GameInfoOverlay::CreateWindowsSizeDependentResources är där all vår ritning sker. Följande är en översikt över metodens steg.

  • Tre rektanglar skapas för att dela upp användargränssnittstexten för Title, Bodyoch Action text.

    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
        );
    
  • En bitmapp skapas med namnet m_levelBitmap, med den aktuella DPI:en i beräkningen med hjälp av CreateBitmap-.

  • m_levelBitmap anges som vårt 2D-återgivningsmål med hjälp av ID2D1DeviceContext::SetTarget.

  • Bitmappen rensas genom att använda ID2D1RenderTarget::Clear för att göra varje pixel svart med .

  • ID2D1RenderTarget::BeginDraw anropas för att initiera ritningen.

  • DrawText anropas för att rita texten som lagras i m_titleString, m_bodyStringoch m_actionString i lämplig rektangel med motsvarande ID2D1SolidColorBrush.

  • ID2D1RenderTarget::EndDraw anropas för att stoppa alla ritningsåtgärder på m_levelBitmap.

  • En annan bitmapp skapas med CreateBitmap, namngiven m_tooSmallBitmap, för att användas som reserv, som endast visas om visningskonfigurationen är för liten för spelet.

  • Upprepa processen för att rita på m_levelBitmap för m_tooSmallBitmap, den här gången endast rita strängen Paused i brödtexten.

Nu behöver vi bara sex metoder för att fylla texten i våra sex överläggstillstånd!

Representerar speltillstånd

Var och en av de sex överlagringstillstånden i spelet har en motsvarande metod i GameInfoOverlay-objektet. Dessa metoder ritar en variant av överlägget för att kommunicera explicit information till spelaren om själva spelet. Den här kommunikationen representeras med en rubrik och brödtext sträng. Eftersom exemplet redan har konfigurerat resurserna och layouten för den här informationen när den initierades och med GameInfoOverlay::CreateDeviceDependentResources-metoden behöver den bara ange överläggstillståndsspecifika strängar.

Status del av överlägget anges med ett anrop till någon av följande metoder.

Speltillstånd Statusuppsättningsmetod Statusfält
Laddar GameInfoOverlay::SetGameLoading Title
Laddar resurser
Body
Skriver successivt ut '.' för att indikera laddningsaktivitet.
Inledande start-/högpoängsstatistik GameInfoOverlay::SetGameStats Titel
Högsta poäng
kropp
nivåer slutförda #
Totala poäng #
Totala skott #
Nivåstart GameInfoOverlay::SetLevelStart Rubrik
Nivå #
Nivå målbeskrivning
Spelet har pausats GameInfoOverlay::SetPause Titel
Spelet pausat
Body
None
Spelet är slut GameInfoOverlay::SetGameOver Titel
Spelet över
Kropp
Avklarade nivåer #
Totala poäng #
Totala skott #
Avklarade nivåer #
Högsta poäng #
Spelet vanns GameInfoOverlay::SetGameOver Titel
Du VANN!
Body
Nivåer slutförda #
Totala poäng #
Totala skott #
Nivåer slutförda #
Högsta poäng #

Med metoden GameInfoOverlay::CreateWindowSizeDependentResources deklarerade exemplet tre rektangulära områden som motsvarar specifika regioner i överlägget.

Med dessa områden i åtanke ska vi titta på en av de tillståndsspecifika metoderna, GameInfoOverlay::SetGameStatsoch se hur överlägget ritas.

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

Med hjälp av Direct2D-enhetskontexten som objektet GameInfoOverlay initierade fyller den här metoden titel- och innehållsrutorna med svart med hjälp av bakgrundsborsten. Den ritar texten för strängen "High Score" i titelrektangeln och en sträng som innehåller uppdaterad speltillståndsinformation i brödtextrektangeln med den vita textborsten.

Åtgärdsrektangeln uppdateras av ett efterföljande anrop till GameInfoOverlay::SetAction från en metod på GameMain-objektet, som tillhandahåller den speltillståndsinformation som krävs av GameInfoOverlay::SetAction för att fastställa rätt meddelande till spelaren, till exempel "Tryck för att fortsätta".

Överlägget för ett visst tillstånd väljs i GameMain::SetGameInfoOverlay metod så här:

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

Nu har spelet ett sätt att kommunicera textinformation till spelaren baserat på speltillstånd, och vi har ett sätt att byta vad som visas till dem under hela spelet.

Nästa steg

I nästa avsnitt Lägga till kontrollertittar vi på hur spelaren interagerar med exempelspelet och hur indata ändrar speltillstånd.