Udostępnij za pomocą


Dodawanie interfejsu użytkownika

Uwaga / Notatka

Ten temat jest częścią serii samouczków pt. Tworzenie prostej gry na uniwersalną platformę Windows (UWP) z użyciem DirectX. Ten temat pod tym linkiem ustawia kontekst serii.

Teraz, gdy nasza gra ma swoje wizualizacje 3D, nadszedł czas, aby skupić się na dodaniu niektórych elementów 2D, aby gra mogła przekazać opinię na temat stanu gry do gracza. Można to osiągnąć, dodając proste opcje menu i składniki wyświetlacza przeziernego na wyjściu potoku grafiki 3-D.

Uwaga / Notatka

Jeśli nie pobrałeś najnowszego kodu gry tego przykładu, przejdź na przykład gry Direct3D. Ten przykład jest częścią dużej kolekcji przykładów funkcji platformy UWP. Aby uzyskać instrukcje dotyczące pobierania przykładu, zobacz Przykładowe aplikacje do programowania systemu Windows.

Cel

Za pomocą funkcji Direct2D dodaj do naszej gry uwP DirectX szereg elementów graficznych i zachowań interfejsu użytkownika, w tym:

Nakładka interfejsu użytkownika

Chociaż istnieje wiele sposobów wyświetlania tekstu i elementów interfejsu użytkownika w grze DirectX, skupimy się na używaniu direct2D. Będziemy również używać funkcji DirectWrite dla elementów tekstowych.

Direct2D to zestaw interfejsów API rysunku 2D używanych do rysowania elementów pierwotnych i efektów opartych na pikselach. Zaczynając od funkcji Direct2D, najlepiej zachować prostotę. Złożone układy i zachowania interfejsu wymagają czasu i planowania. Jeśli gra wymaga złożonego interfejsu użytkownika, takiego jak te znalezione w grach symulacji i strategii, rozważ użycie języka XAML.

Uwaga / Notatka

Aby uzyskać informacje o tworzeniu interfejsu użytkownika za pomocą języka XAML w grze DirectX platformy UWP, zobacz Rozszerzanie przykładowej gry.

Direct2D nie jest specjalnie zaprojektowany dla interfejsów użytkownika ani układów, takich jak HTML i XAML. Nie udostępnia składników interfejsu użytkownika, takich jak listy, pola ani przyciski. Nie udostępnia również składników układu, takich jak divs, tables lub grids.

W tej przykładowej grze mamy dwa główne składniki interfejsu użytkownika.

  1. Wyświetlacz head-up dla wyników i kontrolek w grze.
  2. Nakładka używana do wyświetlania tekstu stanu gry oraz opcji, takich jak informacje o pauzie i opcje rozpoczęcia poziomu.

Używanie funkcji Direct2D dla wyświetlacza head-up

Na poniższym obrazku przedstawiono wyświetlacz HUD w grze dla próbki. Jest to proste i przejrzyste, co pozwala graczowi skupić się na poruszaniu się po świecie 3D i strzelaniu do celów. Dobry interfejs lub wyświetlacz head-up nigdy nie może komplikować zdolności gracza do przetwarzania i reagowania na wydarzenia w grze.

zrzut ekranu nakładki gry

Nakładka składa się z następujących podstawowych elementów pierwotnych.

  • DirectWrite tekst w prawym górnym rogu, który informuje gracza o
    • Udane trafienia
    • Liczba strzałów wykonanych przez zawodnika
    • Czas pozostały na poziomie
    • Bieżący numer poziomu
  • Dwa przecinające się segmenty linii używane do tworzenia krzyżowych włosów
  • Dwa prostokąty w dolnych rogach definiują granice kontrolera ruchu i wyglądu.

Stan wyświetlania nakładki w grze jest rysowany w metodzie GameHud::Render klasy GameHud. W ramach tej metody nakładka Direct2D reprezentująca nasz interfejs użytkownika jest aktualizowana w celu odzwierciedlenia zmian liczby trafień, pozostałego czasu i liczby poziomów.

Jeśli gra została zainicjowana, dodamy TotalHits(), TotalShots() i TimeRemaining() do bufora swprintf_s i określimy format drukowania. Następnie możemy go narysować przy użyciu metody DrawText . Robimy to samo dla bieżącego wskaźnika poziomu, rysując puste liczby, aby pokazać nieukończone poziomy, takie jak ➀, i wypełnione liczby, takie jak ➊, aby pokazać, że określony poziom został ukończony.

Poniższy fragment kodu przedstawia proces 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
            ...
        }
    }
}

Dalsze podzielenie metody spowoduje, że ten fragment metody GameHud::Render rysuje nasze prostokąty ruchu i ognia za pomocą ID2D1RenderTarget::DrawRectangleoraz celowniki przy użyciu dwóch wywołań do 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
        );
}

W metodzie GameHud::Render przechowujemy logiczny rozmiar okna gry w zmiennej windowBounds. GetLogicalSize Używa to metody klasy DeviceResources.

auto windowBounds = m_deviceResources->GetLogicalSize();

Uzyskanie rozmiaru okna gry jest niezbędne do programowania interfejsu użytkownika. Rozmiar okna jest podawany w pomiarze o nazwie DIPs (niezależne od urządzenia piksele), gdzie DIP jest zdefiniowany jako 1/96 cala. Direct2D skaluje jednostki rysunku do rzeczywistych pikseli podczas wykonywania rysunku, używając ustawienia DPI systemu Windows. Podobnie podczas rysowania tekstu przy użyciu DirectWritenależy określić adresy IP, a nie punkty dla rozmiaru czcionki. Adresy IP są wyrażane jako liczby zmiennoprzecinkowe. 

Wyświetlanie informacji o stanie gry

Oprócz wyświetlacza head-up, przykładowa gra ma nakładkę, która reprezentuje sześć stanów gry. Wszystkie etapy zawierają duży prostokąt podstawowy z tekstem do odczytania przez gracza. Prostokąty kontrolera ruchu i celowniki nie są rysowane, ponieważ nie są aktywne w tych stanach.

Nakładka jest tworzona przy użyciu klasy GameInfoOverlay, co umożliwia nam zmianę wyświetlanego tekstu, aby był zgodny ze stanem gry.

stan i akcja nakładki

Nakładka jest podzielona na dwie sekcje: Stan i Akcja. Sekcja Stan jest dalej podzielona na prostokąty Tytuł i Treść . Sekcja Akcja zawiera tylko jeden prostokąt. Każdy prostokąt ma inny cel.

  • titleRectangle zawiera tekst tytułu.
  • bodyRectangle zawiera tekst treści.
  • actionRectangle zawiera tekst informujący odtwarzacza o podjęciu określonej akcji.

Gra ma sześć stanów, które można ustawić. Stan gry przekazany przy użyciu Status części nakładki. Prostokąty statusu są aktualizowane za pomocą kilku metod odpowiadających następującym stanom.

  • Ładowanie
  • Początkowe statystyki rozpoczęcia/wysokiej oceny
  • Początek poziomu
  • Wstrzymano grę
  • Koniec gry
  • Wygrana w grze

Część Akcji z nakładki jest aktualizowana przy użyciu metody GameInfoOverlay::SetAction, co umożliwia ustawienie tekstu akcji na jedną z następujących wartości.

  • "Naciśnij, aby odtworzyć ponownie..."
  • Trwa ładowanie poziomu, proszę czekać ...
  • "Naciśnij, aby kontynuować..."
  • Żaden

Uwaga / Notatka

Obie te metody zostaną omówione bardziej szczegółowo w sekcji Reprezentacja stanu gry .

W zależności od tego, co się dzieje w grze, pola tekstowe w sekcji Status i Akcja są dostosowywane. Przyjrzyjmy się, jak inicjujemy i narysujemy nakładkę dla tych sześciu stanów.

Inicjowanie i rysowanie nakładki

Sześć stanów Status ma kilka wspólnych elementów, co sprawia, że zasoby i metody, których potrzebują, są bardzo podobne. - Wszystkie używają czarnego prostokąta w środku ekranu jako tła. — Wyświetlany tekst to tekst tytuł lub tekst treść. - Tekst używa czcionki Segoe UI i jest rysowany na tle prostokąta.

Przykładowa gra ma cztery metody, które są wykorzystywane podczas tworzenia nakładki.

GameInfoOverlay::GameInfoOverlay

Konstruktor GameInfoOverlay::GameInfoOverlay inicjuje nakładkę, zachowując powierzchnię bitmapy, która będzie używana do wyświetlania informacji graczowi. Konstruktor uzyskuje fabrykę z obiektu ID2D1Device przekazany do niego, którego używa do utworzenia ID2D1DeviceContext, do którego może rysować sam obiekt nakładki. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources to nasza metoda tworzenia pędzli, które będą używane do rysowania tekstu. W tym celu uzyskujemy obiekt ID2D1DeviceContext2 , który umożliwia tworzenie i rysowanie geometrii oraz funkcje, takie jak renderowanie siatki atramentowej i gradientowej. Następnie tworzymy serię kolorowych pędzli używając ID2D1SolidColorBrush, aby narysować następujące elementy interfejsu użytkownika.

  • Czarny pędzel do prostokątnych teł
  • Biały pędzl dla tekstu stanu
  • Pomarańczowy pędzl do tekstu akcji

DeviceResources::SetDpi

Metoda DeviceResources::SetDpi ustawia kropki na cal okna. Ta metoda jest wywoływana po zmianie DPI i musi zostać dostosowana, co ma miejsce po zmianie rozmiaru okna gry. Po zaktualizowaniu dpi ta metoda wywołuje również metodęDeviceResources::CreateWindowSizeDependentResources , aby upewnić się, że niezbędne zasoby są ponownie tworzone przy każdej zmianie rozmiaru okna.

GameInfoOverlay::CreateWindowsSizeDependentResources

Metoda GameInfoOverlay::CreateWindowsSizeDependentResources jest miejscem, gdzie odbywa się całe rysowanie. Poniżej przedstawiono konspekt kroków metody.

  • Trzy prostokąty są tworzone w celu wydzielenia tekstu w interfejsie użytkownika dla Title, Treśćoraz Action.

    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
        );
    
  • Mapa bitowa jest tworzona o nazwie m_levelBitmap, biorąc pod uwagę bieżące dpi przy użyciu metody CreateBitmap.

  • m_levelBitmap zostaje ustawiona jako docelowy obiekt renderowania 2D przy użyciu ID2D1DeviceContext::SetTarget.

  • Mapa bitowa jest czyszczona poprzez zamianę każdego piksela na czarny przy użyciu ID2D1RenderTarget::Clear.

  • ID2D1RenderTarget::BeginDraw jest wywoływana w celu zainicjowania renderowania.

  • pl-PL: DrawText jest wywoływany w celu narysowania tekstu przechowywanego w m_titleString, m_bodyString i m_actionString w odpowiednim prostokącie przy użyciu odpowiedniego ID2D1SolidColorBrush.

  • ID2D1RenderTarget::EndDraw jest wywoływana w celu zatrzymania wszystkich operacji rysowania na m_levelBitmap.

  • Kolejna mapa bitowa jest tworzona przy użyciu CreateBitmap, nazwana m_tooSmallBitmap, do użycia jako rezerwowa, wyświetlana tylko wtedy, gdy konfiguracja wyświetlania jest zbyt mała do gry.

  • Powtórz proces rysowania na m_levelBitmap dla m_tooSmallBitmap, tym razem rysując tylko napis Paused w ciele.

Teraz potrzeba sześciu metod, aby wypełnić tekst naszych sześciu stanów nakładki!

Reprezentowanie stanu gry

Każdy z sześciu stanów nakładki w grze ma odpowiadającą mu metodę w obiekcie GameInfoOverlay. Te metody tworzą wariant nakładki, aby przekazać graczowi jawne informacje o grze. Ta komunikacja jest reprezentowana za pomocą ciągu Tytuł i Treść . Ponieważ przykład skonfigurował już zasoby i układ dla tych informacji, gdy został zainicjowany, i z metody GameInfoOverlay::CreateDeviceDependentResources, musi tylko podać ciągi specyficzne dla stanu nakładki.

Część Stan nakładki jest ustawiana za pomocą wywołania jednej z następujących metod.

Stan gry Metoda ustawiania stanu Pola stanu
Ładowanie GameInfoOverlay::SetGameLoading Tytuł
Ładowanie zasobów
Treść
drukuje kropki '.' przyrostowo, aby zasygnalizować działanie ładowania.
Początkowe statystyki rozpoczęcia/wysokiej oceny GameInfoOverlay::SetGameStats Tytuł
wysokiej oceny
poziomów ciała
Ukończono #
Łączna liczba punktów #
Łączna liczba ujęć #
Początek poziomu GameInfoOverlay::SetLevelStart Tytuł
#

Opis celu poziomu.
Wstrzymano grę GameInfoOverlay::SetPause Tytuł gry
Wstrzymano
Body
None
Koniec gry GameInfoOverlay::SetGameOver Tytuł
Gra zakończona
Treść
Poziomy ukończone #
Łączna liczba punktów #
Łączna liczba strzałów #
Poziomy ukończone #
Wysoki wynik #
Wygrana w grze GameInfoOverlay::SetGameOver Tytuł
WYGRAŁEŚ!
Ciała ukończone poziomy

Łączna liczba punktów #
Łączna liczba ujęć
Ukończone poziomy
Wysoki wynik #

W przypadku metody GameInfoOverlay::CreateWindowSizeDependentResources przykład zadeklarował trzy prostokątne obszary, które odpowiadają określonym regionom nakładki.

Mając na uwadze te obszary, przyjrzyjmy się jednej z metod specyficznych dla stanu gry, GameInfoOverlay::SetGameStats, i zobaczmy, jak nakładka jest rysowana.

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

Korzystając z kontekstu urządzenia Direct2D zainicjowanego przez obiekt GameInfoOverlay, ta metoda wypełnia za pomocą pędzla tła prostokąty tytułu i treści na czarno. Rysuje tekst ciągu "Wysoki wynik" w prostokącie tytułu oraz ciąg zawierający zaktualizowane informacje o stanie gry w prostokącie treści, używając pędzla koloru białego do tekstu.

Prostokąt działań jest aktualizowany przez następne wywołanie GameInfoOverlay::SetAction z metody obiektu GameMain, który dostarcza informacje o stanie gry potrzebne przez GameInfoOverlay::SetAction w celu określenia odpowiedniego komunikatu dla gracza, takiego jak "Naciśnij, aby kontynuować".

Nakładka dla dowolnego stanu jest wybierana w GameMain::SetGameInfoOverlay metodą w następujący sposób:

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

Teraz gra ma sposób przekazywania informacji tekstowych do gracza na podstawie stanu gry i mamy sposób przełączania, co jest wyświetlane im w całej grze.

Dalsze kroki

W następnym temacie Dodawanie kontroli, zobaczymy, jak gracz wchodzi w interakcję z przykładową grą oraz jak wejście zmienia stan gry.