Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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:
- Head-up-display, inklusive begränsningsrektanglar för rörelse- och tittkontroll
- Speltillståndsmenyer
Ö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.
- En heads-up-skärm för poängen och kontrollerna i spelet.
- 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.
Ö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
- Skapa en bitmapp med **ID2D1RenderTarget::DrawBitmap**
- Dela upp användargränssnittsområden i rektanglar med hjälp av D2D1::RectF
- Använda DrawText för att skapa textelement
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.
Ö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.
-
titleRectangleinnehåller rubriktexten. -
bodyRectangleinnehåller brödtexten. -
actionRectangleinnehå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_levelBitmapanges 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_bodyStringochm_actionStringi 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_levelBitmapförm_tooSmallBitmap, den här gången endast rita strängenPausedi 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.