Aggiungere un'interfaccia utente

Nota

Questo argomento fa parte della serie di esercitazioni Creare un semplice gioco UWP (Universal Windows Platform) con DirectX. L'argomento in tale collegamento imposta il contesto per la serie.

Ora che il nostro gioco ha i suoi oggetti visivi 3D, è il momento di concentrarsi sull'aggiunta di alcuni elementi 2D in modo che il gioco possa fornire un feedback sullo stato di gioco al giocatore. A tale scopo, è possibile aggiungere semplici opzioni di menu e componenti heads-up display nella parte superiore dell'output della pipeline grafica 3D.

Nota

Se non è ancora stato scaricato il codice di gioco più recente per questo esempio, andare in Esempio di gioco Direct3D. Questo esempio fa parte di una vasta raccolta di esempi di funzionalità UWP. Per istruzioni su come scaricare l'esempio, vedere Applicazioni di esempio per lo sviluppo di Windows.

Obiettivo

Utilizzando Direct2D, aggiungere una serie di grafiche e comportamenti dell'interfaccia utente al gioco UWP DirectX, tra cui:

La sovrimpressione dell'interfaccia utente

Anche se esistono molti modi per visualizzare elementi di testo e interfaccia utente in un gioco DirectX, ci concentreremo sull'uso di Direct2D. Utilizzeremo anche DirectWrite per gli elementi di testo.

Direct2D è una serie di API di disegno 2D utilizzate per disegnare primitive ed effetti basati su pixel. Quando si inizia con Direct2D, è consigliabile mantenere le cose semplici. I layout complessi e i comportamenti dell'interfaccia richiedono tempo e pianificazione Se il gioco richiede un'interfaccia utente complessa, ad esempio quelle presenti nei giochi di simulazione e strategia, è consigliabile utilizzare XAML.

Nota

Per informazioni sullo sviluppo di un'interfaccia utente con XAML in un gioco UWP DirectX, vedere Estendere il gioco di esempio.

Direct2D non è progettato specificamente per interfacce utente o layout come HTML e XAML. Non fornisce componenti dell'interfaccia utente come elenchi, caselle o pulsanti. Non fornisce inoltre componenti di layout come divisioni, tabelle o griglie.

Per questo gioco di esempio sono disponibili due componenti principali dell'interfaccia utente.

  1. Un heads-up display per il punteggio e controlli in-game.
  2. Una sovrimpressione utilizzata per visualizzare il testo dello stato del gioco e opzioni, quali informazioni di pausa e opzioni di avvio livello.

Uso di Direct2D per un heads-up display

L'immagine seguente mostra l'heads-up display in-game per l'esempio. È semplice e ordinato, consentendo al giocatore di concentrarsi sulla navigazione nel mondo 3D e sugli obiettivi di tiro. Una buona interfaccia o un buon heads-up display non deve mai complicare la capacità del giocatore di elaborare e reagire agli eventi nel gioco.

screenshot della sovrimpressione del gioco

La sovrimpressione è costituita dalle seguenti primitive di base.

  • Testo DirectWrite nell'angolo superiore destro che informa il giocatore su
    • Colpi andati a segno
    • Numero di colpi effettuati dal giocatore
    • Tempo residuo nel livello
    • Numero di livello corrente
  • Due segmenti di linea che si intersecano utilizzati per formare un mirino
  • Due rettangoli negli angoli inferiori per i limiti del controller move-look.

Lo stato heads-up display in-game della sovrimpressione è disegnato nel metodo GameHud::Render della classe GameHud. All'interno di questo metodo, la sovrimpressione Direct2D che rappresenta la nostra interfaccia utente viene aggiornata per riflettere le modifiche apportate al numero di colpi, al tempo residuo e al numero di livello.

Se il gioco è stato inizializzato, aggiungiamo TotalHits(), TotalShots() e TimeRemaining() a un buffer swprintf_s e specifichiamo il formato di stampa. Possiamo quindi disegnarlo utilizzando il metodo DrawText. Facciamo lo stesso per l'indicatore di livello corrente, disegnando numeri vuoti per mostrare livelli non compilati come ➀ e numeri compilati come ➊ per mostrare che il livello specifico è stato completato.

Il frammento di codice seguente illustra il processo del metodo GameHud::Render per

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

Suddividendo ulteriormente il metodo, questo elemento del metodo GameHud::Render disegna i nostri rettangoli di spostamento e sparo con ID2D1RenderTarget::DrawRectangle e i mirini utilizzando due chiamate a 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
        );
}

Nel metodo GameHud::Render archiviamo le dimensioni logiche della finestra di gioco nella variabile windowBounds. Viene utilizzato il metodo GetLogicalSize della classe DeviceResources.

auto windowBounds = m_deviceResources->GetLogicalSize();

Ottenere le dimensioni della finestra di gioco è essenziale per la programmazione dell'interfaccia utente. Le dimensioni della finestra vengono specificate in una misurazione denominata DIP (Device Independent Pixel), dove un DIP viene definito come 1/96 di pollice. Direct2D ridimensiona le unità di misura del disegno in pixel effettivi quando si verifica il disegno, utilizzando l'impostazione punti per pollice (DPI) di Windows. Analogamente, quando si disegna testo usando DirectWrite, si specificano DIP anziché punti per le dimensioni del font. I DIP vengono espressi come numeri a virgola mobile. 

Visualizzazione delle informazioni sullo stato di gioco

Oltre all'heads-up display, il gioco di esempio ha una sovrimpressione che rappresenta sei stati di gioco. Tutti gli stati presentano una primitiva rettangolare nera di grandi dimensioni con testo da leggere per il giocatore. I rettangoli e i mirini del controller move-look non vengono disegnati perché non sono attivi in questi stati.

La sovrimpressione viene creata utilizzando la classe GameInfoOverlay, che ci consente di sostituire il testo visualizzato per allinearci allo stato di gioco.

stato e azione della sovrimpressione

La sovrimpressione è suddivisa in due sezioni: Stato e Azione. La sezione Stato è ulteriormente suddivisa nei rettangoli Titolo e Corpo. La sezione Azione presenta un solo rettangolo. Ogni rettangolo ha uno scopo diverso.

  • titleRectangle contiene il testo del titolo.
  • bodyRectangle contiene il testo del corpo.
  • actionRectangle contiene il testo che informa il giocatore di eseguire un'azione specifica.

Il gioco ha sei stati che è possibile impostare. Lo stato del gioco trasmesso tramite la parte Stato della sovrimpressione. I rettangoli Stato vengono aggiornati tramite una serie di metodi diversi corrispondenti agli stati seguenti.

  • Caricamento
  • Statistiche iniziali di avvio/punteggio più alto
  • Avvio livello
  • Gioco sospeso
  • Gioco concluso
  • Gioco vinto

La parte Azione della sovrimpressione viene aggiornata tramite il metodo GameInfoOverlay::SetAction, che consente l'impostazione del testo dell'azione in una delle seguenti opzioni.

  • "Tocca per giocare di nuovo..."
  • "Caricamento livello, attendere ..."
  • "Tocca per continuare..."
  • Nessuna

Nota

Entrambi questi metodi verranno illustrati più avanti nella sezione Rappresentazione dello stato di gioco.

A seconda degli elementi in corso nel gioco, i campi di testo della sezione Stato e Azione vengono regolati. Vediamo come inizializzare e disegnare la sovrimpressione per questi sei stati.

Inizializzazione e disegno della sovrimpressione

I sei stati Status hanno alcune cose in comune, rendendo le risorse e i metodi a loro necessari molto simili. - Tutti utilizzano un rettangolo nero al centro dello schermo come sfondo. - Il testo visualizzato è un testo Titolo o Corpo. - Il testo utilizza il font Segoe UI e viene disegnato nella parte superiore del rettangolo posteriore.

Il gioco di esempio ha quattro metodi che entrano in gioco durante la creazione della sovrimpressione.

GameInfoOverlay::GameInfoOverlay

Il costruttore GameInfoOverlay::GameInfoOverlay inizializza la sovrimpressione, mantenendo la superficie bitmap che utilizzeremo per visualizzare informazioni al giocatore. Il costruttore ottiene una factory dall'oggetto ID2D1Device che gli è stato trasferito, che utilizza per creare un oggetto ID2D1DeviceContext su cui l'oggetto sovraimpresso può disegnare direttamente. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources è il nostro metodo per la creazione di pennelli che saranno utilizzati per disegnare il nostro testo. A tale scopo, ottieniamo un oggetto ID2D1DeviceContext2 che consente la creazione e il disegno della geometria, oltre a funzionalità quali il rendering di inchiostro e mesh a gradiente. Creiamo quindi una serie di pennelli colorati utilizzando ID2D1SolidColorBrush per disegnare i seguenti elementi dell'interfaccia utente.

  • Pennello nero per gli sfondi rettangolari
  • Pennello bianco per il testo dello stato
  • Pennello arancione per il testo dell'azione

DeviceResources::SetDpi

Il metodo DeviceResources::SetDpi imposta i punti per pollice della finestra. Questo metodo viene chiamato quando il valore DPI viene modificato e deve essere regolato nuovamente, il che si verifica quando la finestra del gioco viene ridimensionata. Dopo l'aggiornamento del valore DPI, questo metodo chiama anche DeviceResources::CreateWindowSizeDependentResources per accertarsi che le risorse necessarie vengano ricreate ogni volta che la finestra viene ridimensionata.

GameInfoOverlay::CreateWindowsSizeDependentResources

Il metodo GameInfoOverlay::CreateWindowsSizeDependentResources è il luogo in cui viene eseguito tutto il nostro disegno. Di seguito è riportato uno schema dei passaggi del metodo.

  • Vengono creati tre rettangoli per dividere il testo dell'interfaccia utente per il testo Titolo, Corpo e Azione.

    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
        );
    
  • Viene creata una bitmap denominata m_levelBitmap, tenendo conto del valore DPI corrente utilizzando CreateBitmap.

  • m_levelBitmap viene impostato come target di rendering 2D utilizzando ID2D1DeviceContext::SetTarget.

  • La bitmap viene cancellata con ogni pixel reso nero utilizzando ID2D1RenderTarget::Clear.

  • ID2D1RenderTarget::BeginDraw viene chiamato per avviare il disegno.

  • DrawText viene chiamato per disegnare il testo archiviato in m_titleString, m_bodyString e m_actionString nel rettangolo appropriato utilizzando l'ID2D1SolidColorBrush corrispondente.

  • ID2D1RenderTarget::EndDraw viene chiamato per arrestare tutte le operazioni di disegno su m_levelBitmap.

  • Viene creata un'altra bitmap utilizzando CreateBitmap denominata m_tooSmallBitmap da utilizzare come fallback, che mostra solo se la configurazione di visualizzazione è troppo piccola per il gioco.

  • Ripetere il processo di disegno su m_levelBitmap per m_tooSmallBitmap, questa volta disegnando solo la stringa Paused nel corpo.

Ora tutto quello di cui abbiamo bisogno sono sei metodi per riempire il testo dei nostri sei stati di sovrimpressione!

Rappresentazione dello stato del gioco

Ognuno dei sei stati di sovrimpressione nel gioco ha un metodo corrispondente nell'oggetto GameInfoOverlay. Questi metodi tracciano una variante della sovrimpressione per comunicare informazioni esplicite al giocatore sul gioco stesso. Questa comunicazione è rappresentata con una stringa Title e Body. Poiché l'esempio ha già configurato le risorse e il layout per queste informazioni quando è stato inizializzato e con il metodo GameInfoOverlay::CreateDeviceDependentResources, deve solo fornire le stringhe specifiche dello stato di sovrimpressione.

La parte Stato della sovrimpressione viene impostata con una chiamata a uno dei seguenti metodi.

Stato del gioco Metodo impostazione di stato Campi di stato
Caricamento GameInfoOverlay::SetGameLoading Titolo
Caricamento risorse
Corpo
Stampa "." in modo incrementale per suggerire un'attività di caricamento.
Statistiche iniziali di avvio/punteggio più alto GameInfoOverlay::SetGameStats Titolo
Punteggio più alto
Corpo
N. livelli completati
N. punti totali
N. colpi totali
Avvio livello GameInfoOverlay::SetLevelStart Titolo
N. livello
Corpo
Descrizione dell'obiettivo del livello
Gioco sospeso GameInfoOverlay::SetPause Titolo
Gioco sospeso
Corpo
Nulla
Gioco concluso GameInfoOverlay::SetGameOver Titolo
Gioco concluso
Corpo
N. livelli completati
N. punti totali
N. colpi totali
N. livelli completati
N. punteggio più alto
Gioco vinto GameInfoOverlay::SetGameOver Titolo
Hai VINTO!
Corpo
N. livelli completati
N. punti totali
N. colpi totali
N. livelli completati
N. punteggio più alto

Con il metodo GameInfoOverlay::CreateWindowSizeDependentResources, l'esempio ha dichiarato tre aree rettangolari corrispondenti ad aree specifiche della sovrimpressione.

Tenendo presenti queste aree, esaminiamo uno dei metodi specifici dello stato, GameInfoOverlay::SetGameStats, e vediamo come viene disegnata la sovrimpressione.

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

Utilizzando il contesto di dispositivo Direct2D inizializzato dall'oggetto GameInfoOverlay, questo metodo riempie i rettangoli del titolo e del corpo con il nero utilizzando il pennello per lo sfondo. Disegna il testo per la stringa "Punteggio più alto" nel rettangolo del titolo e una stringa contenente le informazioni sullo stato del gioco aggiornate nel rettangolo del corpo usando il pennello per il testo bianco.

Il rettangolo di azione viene aggiornato da una chiamata successiva a GameInfoOverlay::SetAction da un metodo sull'oggetto GameMain, che fornisce le informazioni sullo stato del gioco necessarie da GameInfoOverlay::SetAction per determinare il messaggio corretto al giocatore, ad esempio "Tocca per continuare".

La sovrimpressione per qualsiasi altro stato specificato viene scelta nel metodo GameMain::SetGameInfoOverlay come segue:

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

Ora il gioco ha un modo per comunicare le informazioni di testo al giocatore in base allo stato del gioco e abbiamo un modo per passare a ciò che viene visualizzato ai giocatori durante tutto il gioco.

Passaggi successivi

Nell'argomento successivo, Aggiunta di controlli, illustriamo come il giocatore interagisce con il gioco di esempio e il modo in cui un input cambia lo stato del gioco.