Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Opmerking
Dit onderwerp maakt deel uit van de tutorialreeks Een eenvoudig Universal Windows Platform (UWP) spel maken met DirectX. In het onderwerp op die koppeling wordt de context voor de reeks ingesteld.
Nu ons spel de 3D-visuals bevat, is het tijd om ons te concentreren op het toevoegen van enkele 2D-elementen, zodat het spel feedback kan geven over de gamestatus aan de speler. Dit kan worden bereikt door eenvoudige menuopties en heads-up display-componenten toe te voegen boven op de uitvoer van de 3D-grafische pijplijn.
Opmerking
Als u de nieuwste gamecode voor dit voorbeeld nog niet hebt gedownload, gaat u naar Direct3D-voorbeeldspel. Dit voorbeeld maakt deel uit van een grote verzameling UWP-functievoorbeelden. Zie Voorbeeldtoepassingen voor Windows-ontwikkelingvoor instructies over het downloaden van het voorbeeld.
Doelstelling
Voeg met Direct2D een aantal afbeeldingen en gedragingen van de gebruikersinterface toe aan onze UWP DirectX-game, waaronder:
- Head-up display, inclusief grensrechthoeken van de move-look-controller
- Spelstatusmenu's
De overlay van de gebruikersinterface
Hoewel er veel manieren zijn om tekst- en gebruikersinterface-elementen weer te geven in een DirectX-game, gaan we ons richten op het gebruik van Direct2D. We gebruiken ook DirectWrite voor de tekstelementen.
Direct2D is een set 2D-teken-API's die worden gebruikt om pixelgebaseerde primitieven en effecten te tekenen. Wanneer u begint met Direct2D, kunt u het beste alles eenvoudig houden. Complexe indelingen en interfacegedrag hebben tijd en planning nodig. Als uw game een complexe gebruikersinterface vereist, zoals die in simulatie- en strategiegames, kunt u in plaats daarvan XAML gebruiken.
Opmerking
Zie Het voorbeeldspel uitbreiden voor informatie over het ontwikkelen van een gebruikersinterface met XAML in een UWP DirectX-game.
Direct2D is niet specifiek ontworpen voor gebruikersinterfaces of indelingen zoals HTML en XAML. Het biedt geen onderdelen van de gebruikersinterface, zoals lijsten, vakken of knoppen. Het biedt ook geen indelingsonderdelen zoals div's, tabellen of rasters.
Voor dit voorbeeldspel hebben we twee belangrijke UI-onderdelen.
- Een heads-up display voor score en in-game bediening.
- Een overlay die wordt gebruikt om tekst en opties voor de gamestatus weer te geven, zoals pauze-informatie en opties bij het begin van een level.
Direct2D gebruiken voor een heads-upscherm
In de volgende afbeelding ziet u de in-game HUD als voorbeeld. Het is eenvoudig en overzichtelijk, zodat de speler zich kan concentreren op het navigeren in de 3D-wereld en schietdoelen. Een goede interface of heads-up display mag de mogelijkheid van de speler om de gebeurtenissen in het spel te verwerken en erop te reageren nooit bemoeilijken.
De overlay bestaat uit de volgende basisprimitieven.
-
DirectWrite tekst in de rechterbovenhoek die de speler informeert
- Geslaagde treffers
- Aantal schoten dat de speler heeft gemaakt
- Resterende tijd in het niveau
- Nummer van het huidige niveau
- Twee snijlijnsegmenten die worden gebruikt om een kruishaar te vormen
- Twee rechthoeken in de onderste hoeken binnen de grenzen van de verplaats- en kijkcontroller.
De in-game status van de heads-up display van de overlay wordt weergegeven in de GameHud::Render methode van de GameHud klasse. Binnen deze methode wordt de Direct2D-overlay die onze gebruikersinterface vertegenwoordigt bijgewerkt om de wijzigingen in het aantal treffers, resterende tijd en niveaunummer weer te geven.
Als het spel is geïnitialiseerd, voegen we TotalHits(), TotalShots(), en TimeRemaining() toe aan een swprintf_s buffer en geven we de afdrukindeling op. We kunnen deze vervolgens tekenen met behulp van de methode DrawText . We doen hetzelfde voor de huidige niveauindicator, waarbij lege getallen worden getekend om niet-voltooide niveaus weer te geven, zoals ➀, en gevulde getallen zoals ➊ om aan te geven dat het specifieke niveau is voltooid.
Het volgende codefragment loopt het proces van de methode GameHud::Render door.
- Een bitmap maken met behulp van **ID2D1RenderTarget::DrawBitmap **
- Delen van UI-gebieden in rechthoeken met behulp van D2D1::RectF
- Gebruik DrawText om tekstelementen te maken
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
...
}
}
}
Door de methode verder op te breken, trekt dit stukje van de GameHud::Render methode onze verplaatsings- en vuurrechthoeken met ID2D1RenderTarget::DrawRectangle, en kruisdraden door middel van twee oproepen naar 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
);
}
In de Methode GameHud::Render slaan we de logische grootte van het gamevenster op in de windowBounds variabele. Hierbij wordt de GetLogicalSize methode van de klasse DeviceResources gebruikt.
auto windowBounds = m_deviceResources->GetLogicalSize();
Het verkrijgen van de grootte van het gamevenster is essentieel voor het programmeren van de gebruikersinterface. De grootte van het venster wordt gegeven in een meting genaamd DIPs (apparaatonafhankelijke pixels), waarbij een DIP wordt gedefinieerd als 1/96 van een inch. Direct2D schaalt de tekeneenheden naar de werkelijke pixels wanneer de tekening plaatsvindt, met behulp van de dpi-instelling (Windows dots per inch). Als u tekst tekent met DirectWrite, geeft u ook DIPs op in plaats van punten voor de grootte van het lettertype. DIPs worden uitgedrukt als drijvende kommagetallen.
Informatie over gamestatus weergeven
Naast de head-updisplay heeft het voorbeeldspel een laag die zes verschillende statussen vertegenwoordigt. Alle staten hebben een grote zwarte rechthoekige vorm met tekst die de speler kan lezen. De rechthoeken en kruisdraden van de verplaatsingscontroller worden niet getekend omdat ze niet actief zijn in deze toestanden.
De overlay wordt gemaakt met behulp van de GameInfoOverlay-klasse , zodat we kunnen overschakelen welke tekst wordt weergegeven om af te stemmen op de status van het spel.
De overlay is onderverdeeld in twee secties: Status en Actie. De sectie Status wordt verder onderverdeeld in rechthoeken Titel en Hoofdtekst . De sectie Actie heeft slechts één rechthoek. Elke rechthoek heeft een ander doel.
-
titleRectanglebevat de titeltekst. -
bodyRectanglebevat de hoofdtekst. -
actionRectanglebevat de tekst die de speler informeert een specifieke actie te ondernemen.
Het spel heeft zes staten die kunnen worden ingesteld. De status van het spel wordt overgebracht via het Status-gedeelte van de overlay. De statusrechthoeken worden bijgewerkt met behulp van een aantal methoden die overeenkomen met de volgende statussen.
- Aan het laden
- Beginnende/hoogste scorestatistieken
- Beginniveau
- Game onderbroken
- Spel voorbij
- Spel gewonnen
Het actiegedeelte van de overlay wordt bijgewerkt met behulp van de methode GameInfoOverlay::SetAction , zodat de actietekst op een van de volgende manieren kan worden ingesteld.
- Tik om opnieuw af te spelen...
- "Niveau laden, een ogenblik geduld ..."
- "Tik om door te gaan..."
- Geen
Opmerking
Beide methoden worden verder besproken in de sectie Representatie van de spelstatus.
Afhankelijk van wat er in het spel gebeurt, worden de tekstvelden status en actie aangepast. Laten we eens kijken hoe we de overlay voor deze zes statussen initialiseren en tekenen.
De overlay initialiseren en tekenen
De zes Status staten hebben een aantal dingen gemeen, waardoor de benodigde resources en methoden erg op elkaar lijken. - Ze gebruiken allemaal een zwarte rechthoek in het midden van het scherm als achtergrond. - De weergegeven tekst is Titel of Hoofdtekst. - De tekst maakt gebruik van het lettertype Segoe UI en wordt op de achtergrondrechthoek getekend.
Het voorbeeldspel heeft vier methoden die in het spel komen bij het maken van de overlay.
GameInfoOverlay::GameInfoOverlay
De GameInfoOverlay::GameInfoOverlay-constructor initialiseert de overlay, waarbij het bitmapoppervlak wordt onderhouden dat we gebruiken om informatie weer te geven aan de speler. De constructor verkrijgt een factory van het ID2D1Device-object dat eraan wordt doorgegeven, wat wordt gebruikt om een ID2D1DeviceContext te maken waarop het overlay-object zelf kan tekenen. IDWriteFactory::CreateTextFormat
GameInfoOverlay::CreateDeviceDependentResources
GameInfoOverlay::CreateDeviceDependentResources is onze methode voor het maken van borstels die worden gebruikt om onze tekst te tekenen. Hiervoor verkrijgen we een ID2D1DeviceContext2-object dat het maken en tekenen van geometrie mogelijk maakt, evenals functionaliteit voor het renderen van inkt en verloopmeshes. Vervolgens maken we een reeks gekleurde borstels met id2D1SolidColorBrush om de volgende UI-elementen te tekenen.
- Zwarte borstel voor rechthoekachtergronden
- Wit penseel voor statustekst
- Oranje penseel voor actietekst
DeviceResources::SetDpi
Met de methode DeviceResources::SetDpi worden de punten per inch van het venster ingesteld. Deze methode wordt aangeroepen wanneer de DPI verandert en opnieuw moet worden afgesteld, wat gebeurt wanneer de grootte van het gamevenster wordt aangepast. Na het bijwerken van de DPI roept deze methode ookDeviceResources::CreateWindowSizeDependentResources aan om ervoor te zorgen dat de benodigde resources opnieuw worden gemaakt wanneer het formaat van het venster wordt gewijzigd.
GameInfoOverlay::CreateWindowsSizeDependentResources
De GameInfoOverlay::CreateWindowsSizeDependentResources methode is waar al onze tekeningen plaatsvinden. Hier volgt een overzicht van de stappen van de methode.
Er worden drie rechthoeken gemaakt om de UI-tekst voor de Titel, Hoofdtekst, en Actie te scheiden.
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 );Er wordt een bitmap gemaakt met de naam
m_levelBitmap, rekening houdend met de huidige DPI met behulp van CreateBitmap.m_levelBitmapis ingesteld als ons 2D-renderdoel met id2D1DeviceContext::SetTarget.De bitmap wordt gewist waarbij elke pixel zwart wordt gemaakt met ID2D1RenderTarget::Clear.
ID2D1RenderTarget::BeginDraw wordt aangeroepen om tekening te initiëren.
DrawText wordt aangeroepen om de tekst te tekenen die is opgeslagen in
m_titleString,m_bodyStringenm_actionStringin de juiste rechthoek met behulp van de bijbehorende ID2D1SolidColorBrush.ID2D1RenderTarget::EndDraw wordt aangeroepen om alle tekenbewerkingen op
m_levelBitmapte stoppen.Er wordt een andere bitmap gemaakt met behulp van CreateBitmap genaamd
m_tooSmallBitmapom als terugval te gebruiken, die alleen wordt getoond als de weergaveconfiguratie te klein is voor het spel.Herhaal dit proces voor tekenen op
m_levelBitmapvoorm_tooSmallBitmap, maar teken deze keer alleen de tekenreeksPausedin het lichaam.
Nu hebben we alleen zes methoden nodig om de tekst van onze zes overlaystatussen te vullen.
De spelsituatie weergeven
Elk van de zes overlaystatussen in het spel heeft een bijbehorende methode in het GameInfoOverlay-object. Met deze methoden wordt een variatie van de overlay getekend om expliciete informatie aan de speler over het spel zelf te communiceren. Deze communicatie wordt weergegeven met een Titel en een Hoofdtekst. Omdat het voorbeeld de resources en indeling voor deze informatie al had geconfigureerd toen het werd geïnitialiseerd en met de GameInfoOverlay::CreateDeviceDependentResources methode, hoeft het alleen de voor de overlaystatus specifieke tekenreeksen op te geven.
Het statusgedeelte van de overlay wordt ingesteld met een aanroep naar een van de volgende methoden.
| Spelstatus | Methode Statusset | Statusvelden |
|---|---|---|
| Aan het laden | GameInfoOverlay::SetGameLoading- |
Titel Laden van resources Hoofdtekst Drukt geleidelijk '.' af om de laadactiviteit aan te duiden. |
| Beginnende/hoogste scorestatistieken | GameInfoOverlay::SetGameStats |
Titel Hoogste Score Inhoud Voltooide Levels # Totale Punten # Totale Schoten # |
| Beginniveau | GameInfoOverlay::SetLevelStart |
Title Level # Hoofdtekst Niveau doelbeschrijving. |
| Game onderbroken | GameInfoOverlay::SetPause |
Titel Spel Gepauzeerd Inhoud Geen |
| Spel voorbij | GameInfoOverlay::SetGameOver |
Titel Game Over Body Voltooide Levels # Totale Punten # Totale Schoten # Voltooide Levels # Highscore # |
| Spel gewonnen | GameInfoOverlay::SetGameOver |
Titel U GEWONNEN! Body Voltooide Levels # Totaal aantal punten # Totaal aantal schoten # Voltooide Levels # Hoogste score # |
Met de methode GameInfoOverlay::CreateWindowSizeDependentResources heeft het voorbeeld drie rechthoekige gebieden gedeclareerd die overeenkomen met specifieke regio's van de overlay.
Met deze gebieden in gedachten kijken we naar een van de statusspecifieke methoden GameInfoOverlay::SetGameStats en kijken hoe de overlay wordt getekend.
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);
}
}
Met behulp van de Direct2D-apparaatcontext, die het GameInfoOverlay object heeft geïnitialiseerd, vult deze methode de titel- en tekstvakken met zwart door middel van het achtergrondpensel. Tekent de tekst voor de string 'Hoge score' in de titelrechthoek en een string met de bijgewerkte gamestatusinformatie in de hoofdrechthoek met behulp van de witte tekstborstel.
De actierechthoek wordt bijgewerkt door een volgende aanroep naar GameInfoOverlay::SetAction- van een methode op het object GameMain, dat de gamestatusgegevens bevat die nodig zijn voor GameInfoOverlay::SetAction- om het juiste bericht voor de speler te bepalen, zoals 'Tikken om door te gaan'.
De overlay voor een bepaalde status wordt gekozen in de methode GameMain::SetGameInfoOverlay als volgt:
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 heeft het spel een manier om tekstinformatie te communiceren aan de speler op basis van de spelstatus, en we hebben een manier om te wisselen wat er aan hen wordt getoond gedurende het spel.
Volgende stappen
In het volgende onderwerp, Besturingselementen toevoegen, bekijken we hoe de speler interageert met het voorbeeldspel en hoe de invoer de game status verandert.