Megosztás a következőn keresztül:


Felhasználói felület hozzáadása

Megjegyzés:

Ez a témakör a Egyszerű Univerzális Windows Platform (UWP) játék készítése DirectX-szel című oktatóanyag-sorozat része. A hivatkozás témaköre beállítja a sorozat kontextusát.

Most, hogy a játék 3D-s vizualizációkkal rendelkezik, ideje néhány 2D-s elem hozzáadására összpontosítani, hogy a játék visszajelzést küldjön a játék állapotáról a játékosnak. Ez úgy érhető el, hogy egyszerű menüopciókat és head-up kijelző összetevőket ad hozzá a 3D grafikai adatfolyam kimenetéhez.

Megjegyzés:

Ha még nem töltötte le a minta legújabb játékkódját, lépjen tovább a Direct3D minta játék-ra. Ez a minta az UWP-szolgáltatásminták nagy gyűjteményének része. A minta letöltésére vonatkozó utasításokért lásd a Windows-fejlesztéshez készült mintaalkalmazásokat.

Célkitűzés

A Direct2D használatával adjon hozzá számos felhasználói felületi ábrát és viselkedést az UWP DirectX-játékhoz, beleértve a következőket:

A felhasználói felület átfedése

Bár a Szöveg és a felhasználói felület elemei számos módon jeleníthetők meg egy DirectX-játékban, Direct2D-fogunk összpontosítani. A szövegelemekhez DirectWrite is használunk.

A Direct2D a képpontalapú primitívek és effektusok rajzolására használt 2D rajz API-k készlete. A Direct2D-vel való kezdéskor a legjobb, ha egyszerűnek tartja a dolgokat. Az összetett elrendezésekhez és felületi viselkedésekhez időre és tervezésre van szükség. Ha a játékhoz összetett felhasználói felületre van szükség, például a szimulációban és a stratégiai játékokban találhatóakhoz, fontolja meg inkább az XAML használatát.

Megjegyzés:

További információ a felhasználói felület XAML-sel való fejlesztéséről egy UWP DirectX-játékban: A mintajáték kiterjesztése.

A Direct2D nem kifejezetten felhasználói felületekhez vagy elrendezésekhez, például HTML-hez és XAML-hez készült. Nem biztosít felhasználói felületi összetevőket, például listákat, dobozokat vagy gombokat. Emellett nem biztosít olyan elrendezési összetevőket, mint a divs, a table vagy a grid.

Ebben a mintajátékban két fő felhasználói felületi összetevőt használunk.

  1. A pontszám és a játékon belüli vezérlők áttekinthető kijelzője.
  2. A játékállapot szövegének és beállításainak, például a szüneteltetési adatoknak és a szintindítási beállításoknak a megjelenítésére szolgáló átfedés.

A Direct2D használata felfelé irányuló kijelzőhöz

Az alábbi képen a minta játékon belüli fej-up kijelzője látható. Egyszerű és áttekinthetetlen, így a játékos a 3D-s világra összpontosíthat, és célokat lőhet ki. A jó interfészek vagy heads-up kijelzők soha nem bonyolíthatják a játékosok képességét arra, hogy feldolgozzák és reagáljanak a játék eseményeire.

a játék keretének képernyőképe

Az átfedés a következő alapvető primitívekből áll.

  • DirectWrite szöveget a jobb felső sarokban, amely tájékoztatja a játékost a játékmenetről.
    • Sikeres találatok
    • A játékos által készített lövések száma
    • A szint hátralévő ideje
    • Aktuális szintszám
  • Két egymást keresztező vonalszegmens, amelyeket keresztszőrzetek alkotnak
  • Két téglalap az alsó sarkokban a mozgatás-nézet vezérlő határaihoz.

Az átfedés játékon belüli megjelenítési állapotát a GameHud::Render GameHud osztályának metódusa rajzolja meg. Ezen a módszeren belül a felhasználói felületet jelképező Direct2D-átfedés frissül, hogy tükrözze a találatok számában, a fennmaradó időben és a szintszámban bekövetkezett változásokat.

Ha a játék inicializálva lett, TotalHits(), TotalShots()és TimeRemaining() hozzáadjuk egy swprintf_s pufferhez, majd megadjuk a nyomtatási formátumot. Ezután megrajzolhatjuk a DrawText metódussal. Ugyanezt tesszük az aktuális szintjelző esetében is, üres számokat rajzolva megjelenítjük a nem befejezett szinteket, például a ➀- és a kitöltött számokat, például a ➊, hogy jelezzük, hogy az adott szint befejeződött.

A következő kódrészlet végigvezeti a GameHud::Render metódus folyamatát a következőhöz:

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

A módszer tovább bontásával ez a GameHud::Render metódus része rajzolja meg a mozgatás és tűz téglalapokat a ID2D1RenderTarget::DrawRectanglesegítségével, és a célkeresztet két hívás segítségével az ID2D1RenderTarget::DrawLine-én.

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

A GameHud::Render metódusban a játékablak logikai méretét a windowBounds változóban tároljuk. Ez a GetLogicalSize osztály metódusát használja.

auto windowBounds = m_deviceResources->GetLogicalSize();

A felhasználói felület programozásához elengedhetetlen a játékablak méretének beszerzése. Az ablak méretét a DIP-k (eszközfüggetlen képpontok) nevű mérés adja meg, ahol a DIP egy hüvelyk 1/96-aként van definiálva. A Direct2D a rajz bekövetkezésekor tényleges képpontokra skálázza a rajzegységeket, ehhez használja a Windows pont/hüvelyk (DPI) beállítást. Hasonlóképpen, ha DirectWritehasználatával rajzol szöveget, a betűtípus méretére vonatkozó pontok helyett a DIP-ket kell megadnia. A DIP-k lebegőpontos számokként vannak kifejezve. 

A játék állapotinformációinak megjelenítése

A heads-up kijelző mellett a mintajátéknak van egy átfedése, amely hat játékállapotot jelöl. Minden állam egy nagy fekete téglalap alapelemet tartalmaz, szöveggel, amit a játékos elolvashat. A move-look vezérlő téglalapjai és célkeresztjei nem rajzolódnak ki, mert ezek az állapotok nem aktívak.

Az átfedés a GameInfoOverlay osztály használatával jön létre, lehetővé téve számunkra, hogy kikapcsoljuk a megjelenő szöveget, hogy igazodjunk a játék állapotához.

átfedés állapota és művelete

Az átfedés két szakaszra oszlik: Állapot és Művelet. Az Állapot szakasz további bontásban a Cím és a Törzs téglalapokra oszlik. A Művelet szakaszban csak egy téglalap van. Minden téglalapnak más a rendeltetése.

  • titleRectangle tartalmazza a címszöveget.
  • bodyRectangle tartalmazza a szövegtörzset.
  • actionRectangle tartalmazza azt a szöveget, amely tájékoztatja a játékost egy adott művelet végrehajtásáról.

A játék hat állapotban állítható be. A játék állapotát a Állapot átfedés részével adják át. A Állapot téglalapok a következő állapotoknak megfelelő számos metódussal frissülnek.

  • Betöltés
  • Kezdeti kezdési/magas pontszámú statisztikák
  • Szint kezdete
  • Játék szüneteltetve
  • Vége a játéknak
  • Játék nyert

Az átfedés művelet része a GameInfoOverlay::SetAction metódussal frissül, így a művelet szövege az alábbiak egyikére állítható be.

  • "Koppintson újra a lejátszáshoz..."
  • Kérjük, várjon, amíg a szint betöltődik...
  • "Koppintson a folytatáshoz..."
  • Egyik sem

Megjegyzés:

Mindkét módszerről bővebben a A játékállapot című szakaszban lesz szó.

A játékban történtektől függően a Állapot és Művelet szakasz szövegmezői módosulnak. Nézzük meg, hogyan inicializáljuk és rajzoljuk meg a hat állam átfedését.

A fedvény inicializálása és rajzolása

A hat státusz államának van néhány közös jellemzője, így az általuk igényelt erőforrások és módszerek nagyon hasonlóak. - Mind fekete téglalapot használnak a képernyő közepén háttérként. - A megjelenített szöveg vagy Cím vagy Törzs szöveg. - A szöveg a Segoe felhasználói felület betűtípusát használja, és a hátsó téglalapra van rajzolva.

A mintajáték négy metódust kínál, amelyek az átfedés létrehozásakor kerülnek játékba.

GameInfoOverlay::GameInfoOverlay

A GameInfoOverlay::GameInfoOverlay konstruktor inicializálja az átfedést, fenntartva a bitképfelületet, amelyen adatokat jelenítünk meg a játékos számára. A konstruktor a neki átadott ID2D1Device objektumból szerez meg egy gyárat, amellyel létrehoz egy ID2D1DeviceContext objektumot, amelyre az átfedési objektum is rajzolhat. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

A GameInfoOverlay::CreateDeviceDependentResources a módszerünk az ecsetek létrehozására, amelyeket a szöveg rajzolásához fogunk használni. Ehhez beszerezünk egy ID2D1DeviceContext2 objektumot, amely lehetővé teszi a geometria létrehozását és rajzolását, valamint olyan funkciók használatát, mint a tinta és a gradiens háló megjelenítése. Ezután létrehozunk egy színes keféket az ID2D1SolidColorBrush használatával a következő felhasználói felületi elemek rajzolásához.

  • Fekete ecset téglalapháttérhez
  • Fehér ecset az állapotszöveghez
  • Narancssárga ecset műveletszöveghez

DeviceResources::SetDpi

A DeviceResources::SetDpi metódus az ablak hüvelykenkénti pontjait állítja be. Ez a metódus a DPI módosításakor lesz meghívva, és újra kell módosítani, ami a játékablak átméretezésekor történik. A DPI frissítése után ez a metódus meghívjaDeviceResources::CreateWindowSizeDependentResources, hogy a szükséges erőforrások minden alkalommal újra létre legyenek hozva az ablak átméretezésekor.

GameInfoOverlay::CreateWindowsSizeDependentResources

A GameInfoOverlay::CreateWindowsSizeDependentResources metódusban történik minden rajzolás. Az alábbiakban a metódus lépéseinek vázlata található.

  • A rendszer három téglalapot hoz létre a Cím, Törzsés Művelet szöveg felhasználói felületének szövegének kimetszéséhez.

    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
        );
    
  • Létrejön egy m_levelBitmapnevű bitkép, amely figyelembe veszi az aktuális DPI-t CreateBitmaphasználatával.

  • m_levelBitmap 2D renderelési céltárgyként van beállítva a ID2D1DeviceContext::SetTargetsegítségével.

  • A bitkép törlődik, minden képpont feketévé válik ID2D1RenderTarget::Clear.

  • ID2D1RenderTarget::BeginDraw meghívására kerül sor a rajzolás megkezdéséhez.

  • A DrawText a m_titleString, m_bodyString és m_actionString tárolt szöveg rajzolására hívható meg a megfelelő téglalapban, az ehhez tartozó ID2D1SolidColorBrush használatával.

  • ID2D1RenderTarget::EndDraw az m_levelBitmapösszes rajzműveletének leállításához hívják meg.

  • Egy másik Bitkép létrehozása CreateBitmapm_tooSmallBitmap használatával történik tartalékként való használatra, csak akkor jelenik meg, ha a megjelenítési konfiguráció túl kicsi a játékhoz.

  • Ismételje meg a folyamatot a m_levelBitmap-ra vonatkozóan a m_tooSmallBitmapesetében, ezúttal csak a sztring Paused-t helyezze el a törzsben.

Most már csak hat módszerre van szükségünk, hogy kitöltsük a hat átfedési állapotunk szövegét!

A játék állapotának jelképe

A játékban szereplő hat átfedési állapot mindegyikének van egy megfelelő metódusa a GameInfoOverlay objektumban. Ezek a módszerek a átfedés egy változatát rajzolják meg, hogy explicit információkat közöljenek a játékossal magáról a játékról. Ez a kommunikáció egy Cím és egy Törzs szöveggel van ábrázolva. Mivel a minta már beállította az adatok erőforrásait és elrendezését az inicializáláskor és a GameInfoOverlay::CreateDeviceDependentResources metódussal, csak az átfedéshelyzet-specifikus szövegeket kell megadnia.

Az átfedés állapotának része az alábbi módszerek egyikének hívásával kerül beállításra.

Játék állapota Állapot beállító metódus Állapotmezők
Betöltés GameInfoOverlay::SetGameLoading Cím
Erőforrások betöltése
Törzs
növekményesen nyomtatja a "." szöveget a tevékenység betöltéséhez.
Kezdeti kezdési/magas pontszámú statisztikák GameInfoOverlay::SetGameStats cím
magas pontszámú
törzs
befejezett szintek #
Összes pont #
Összes felvétel #
Szint kezdete GameInfoOverlay::SetLevelStart Cím
szint #
Törzs
szint cél leírása.
Játék szüneteltetve GameInfoOverlay::SetPause Cím
Játék szüneteltetve
Törzs
Nincs
Vége a játéknak GameInfoOverlay::SetGameOver cím
játék
törzs
befejezett szintek #
Összes pont #
Összes lövés #
Befejezett szintek #
Magas pontszám #
Játék nyert GameInfoOverlay::SetGameOver cím
NYERT!
Teljes
szintek #
Összes pont #
Összes felvétel #
Befejezett szintek #
Magas pontszám #

A GameInfoOverlay::CreateWindowSizeDependentResources metódussal a minta három négyszögletes területet deklarált, amelyek az átfedés adott régióinak felelnek meg.

Ezeket a területeket szem előtt tartva nézzük meg az egyik állapotspecifikus metódust, GameInfoOverlay::SetGameStats, és nézzük meg, hogyan rajzolódik meg az átfedés.

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

A GameInfoOverlay objektum inicializált Direct2D eszközkörnyezetét használva ez a módszer fekete színnel tölti ki a cím és a törzs téglalapjait a háttérkefe használatával. A "High Score" sztring szövegét a cím téglalapjához rajzolja, és egy sztringet, amely a játék állapotadatait frissíti a szövegtörzs téglalapjához a fehér szöveges ecsettel.

A művelet téglalapja frissül a GameInfoOverlay::SetAction GameMain objektum egyik metódusának következő hívásával, amely a GameInfoOverlay::SetAction által igényelt játékállapot-információkat biztosítja a játékosnak megfelelő üzenet meghatározásához, például "Koppintás a folytatáshoz".

Bármely adott állapot átfedését a GameMain::SetGameInfoOverlay metódus választja ki.

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

A játéknak van egy módja, hogy szöveges információkat közöljön a játékossal a játék állapota alapján, és az egész játék során át tudjuk váltani, hogy mi jelenjen meg számukra.

Következő lépések

A következő témakörben, Vezérlők hozzáadása, azt vizsgáljuk meg, hogyan kommunikál a játékos a mintajátékkal, és hogyan változik a bemenet a játék állapotában.