Ajouter une interface utilisateur

Remarque

Cette rubrique fait partie de la série de tutoriels Créer un jeu simple de plateforme Windows universelle simple (UWP) avec DirectX. La rubrique accessible via ce lien définit le contexte de la série.

Maintenant que notre jeu a ses visuels 3D en place, il est temps de se concentrer sur l’ajout d’éléments 2D afin que le jeu puisse fournir des informations sur l’état du jeu au joueur. Pour ce faire, ajoutez de simples options de menu et des composants d’affichage tête haute à la sortie du pipeline graphique 3D.

Remarque

Si vous n’avez pas téléchargé le code du jeu le plus récent pour cet exemple, accédez à Exemple de jeu Direct3D. Cet exemple fait partie d’une grande collection d’exemples de fonctionnalités UWP. Pour obtenir des instructions sur le téléchargement de l’exemple, consultez Exemples d’applications pour le développement Windows.

Objectif

À l’aide de Direct2D, ajoutez un certain nombre de graphiques et de comportements d’interface utilisateur à notre jeu UWP DirectX :

  • L’affichage tête haute, y compris les rectangles de délimitation du contrôleur move-look
  • Les menus d’état du jeu

L’interface utilisateur superposée

Bien qu’il existe de nombreuses façons d’afficher du texte et des éléments d’interface utilisateur dans un jeu DirectX, nous allons nous concentrer sur l’utilisation de Direct2D. Nous utiliserons également DirectWrite pour les éléments textuels.

Direct2D est un ensemble d’API de dessin 2D utilisées pour dessiner des primitives et des effets basés sur des pixels. Lorsque vous commencez à utiliser Direct2D, il est préférable de garder les choses simples. Les mises en page et les comportements d’interface complexes nécessitent du temps et de la planification. Si votre jeu nécessite une interface utilisateur complexe, comme celles que l’on trouve dans les jeux de simulation et de stratégie, envisagez plutôt d’utiliser XAML.

Remarque

Pour plus d’informations sur le développement d’une interface utilisateur avec XAML dans un jeu UWP DirectX, voir Extension de l’exemple de jeu.

Direct2D n’est pas spécifiquement conçu pour les interfaces utilisateur ou les mises en page comme HTML et XAML. Il ne fournit pas de composants d’interface utilisateur tels que des listes, des boîtes ou des boutons. Il ne fournit pas non plus de composants de mise en page comme les divs, les tables ou les grilles.

Pour cet exemple de jeu, nous avons deux composants d’interface utilisateur principaux.

  1. Un affichage tête haute pour le score et les commandes du jeu.
  2. Une superposition utilisée pour afficher le texte de l’état du jeu et les options telles que les informations de pause et les options de début de niveau.

Utilisation de Direct2D pour l’affichage tête haute

L’image suivante montre l’affichage tête haute du jeu pour l’exemple. Il est simple et épuré, permettant au joueur de se concentrer sur la navigation dans le monde 3D et le tir sur les cibles. Une bonne interface ou un bon affichage tête haute ne doit jamais compliquer la capacité du joueur à traiter les événements du jeu et à y réagir.

une capture d’écran de l’affichage du jeu

L’affichage se compose des primitives de base suivantes.

  • Texte DirectWrite dans le coin supérieur droit qui informe le joueur des éléments suivants
    • des coups réussis
    • du nombre de tirs effectués par le joueur
    • Temps restant dans le niveau
    • le numéro du niveau actuel
  • Deux segments de ligne se croisant pour former un réticule.
  • Deux rectangles dans les coins inférieurs pour les limites de la manette move-look.

L’état de l’affichage tête haute dans le jeu est dessiné dans la méthode GameHud::Render de la classe GameHud. Dans cette méthode, l’incrustation Direct2D qui représente notre interface utilisateur est mise à jour pour refléter les changements dans le nombre de coups, le temps restant et le numéro de niveau.

Si le jeu a été initialisé, nous ajoutons TotalHits(), TotalShots() et TimeRemaining() à un tampon swprintf_s et spécifions le format d’impression. Nous pouvons ensuite l’afficher à l’aide de la méthode DrawText. Nous procédons de la même manière pour l’indicateur de niveau actuel, en dessinant des nombres vides pour indiquer les niveaux non terminés, comme ➀, et des nombres remplis, comme ➊, pour indiquer que le niveau en question a été terminé.

L’extrait de code suivant décrit le processus de la méthode GameHud::Render pour

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

En décomposant la méthode, cette partie de la méthode GameHud::Render dessine nos rectangles de déplacement et de tir avec ID2D1RenderTarget::DrawRectangle, et le réticule en utilisant deux appels à 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
        );
}

Dans la méthode GameHud::Render, nous stockons la taille logique de la fenêtre de jeu dans la variable windowBounds. Cette méthode utilise la méthode GetLogicalSize de la classe DeviceResources.

auto windowBounds = m_deviceResources->GetLogicalSize();

L’obtention de la taille de la fenêtre de jeu est essentielle pour la programmation de l’interface utilisateur. La taille de la fenêtre est donnée dans une mesure appelée DIPs (device independent pixels), où un DIP est défini comme 1/96 d’un pouce. Direct2D convertit les unités de dessin en pixels réels lorsque le dessin est effectué, en utilisant le paramètre Windows dots per inch (DPI). De même, lorsque vous dessinez du texte à l’aide de DirectWrite, vous spécifiez des DIP plutôt que des points pour la taille de la police. Les DIP sont exprimés en nombres à virgule flottante. 

Affichage des informations sur l’état du jeu

En plus de l’affichage tête haute, l’exemple de jeu comporte une surcouche qui représente six états de jeu. Tous les états sont dotés d’un grand rectangle noir primitif avec du texte que le joueur peut lire. Les rectangles et le réticule de la manette de visualisation des mouvements ne sont pas dessinés car ils ne sont pas actifs dans ces états.

La superposition est créée à l’aide de la classe GameInfoOverlay, ce qui nous permet de modifier le texte affiché en fonction de l’état du jeu.

état et action de l’incrustation

La superposition est divisée en deux sections : Statut et Action. La section Statut est subdivisée en rectangles Titre et Corps. La section Action ne comporte qu’un seul rectangle. Chaque rectangle a une fonction différente.

  • titleRectangle contient le texte du titre.
  • bodyRectangle contient le texte du corps.
  • actionRectangle contient le texte qui informe le joueur qu’il doit effectuer une action spécifique.

Le jeu a six états qui peuvent être définis. L’état du jeu est transmis par la partie Statut de la superposition. Les rectangles d’état sont mis à jour à l’aide d’un certain nombre de méthodes correspondant aux états suivants.

  • Chargement
  • Stats initiales de début/de haut score
  • Début du niveau
  • Jeu en pause
  • Fin du jeu
  • Partie gagnée

La partie Action de la superposition est mise à jour à l’aide de la méthode GameInfoOverlay::SetAction, ce qui permet de définir le texte de l’action comme suit.

  • "Appuyez pour rejouer..."
  • « Niveau en cours de chargement, veuillez patienter… »
  • "Appuyez pour continuer..."
  • Aucun

Remarque

Ces deux méthodes seront abordées plus en détail dans la section Représentation de l’état du jeu.

En fonction de ce qui se passe dans le jeu, les champs de texte des sections Statut et Action sont ajustés. Voyons comment initialiser et dessiner l’incrustation pour ces six états.

Initialisation et dessin de l’incrustation

Les six états ont quelques points communs, ce qui rend les ressources et les méthodes dont ils ont besoin très similaires. - Ils utilisent tous un rectangle noir au centre de l’écran comme arrière-plan. - Le texte affiché est soit le titre, soit le corps du texte. - Le texte utilise la police Segoe UI et est dessiné au-dessus du rectangle arrière.

L’exemple de jeu comporte quatre méthodes qui entrent en jeu lors de la création de la superposition.

GameInfoOverlay::GameInfoOverlay

Le constructeur GameInfoOverlay::GameInfoOverlay initialise l’incrustation, en maintenant la surface bitmap que nous utiliserons pour afficher les informations au joueur. Le constructeur obtient une fabrique à partir de l’objet ID2D1Device qui lui est passé, qu’il utilise pour créer un ID2D1DeviceContext sur lequel l’objet superposé peut dessiner. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources est notre méthode pour créer les brosses qui seront utilisées pour dessiner notre texte. Pour ce faire, nous obtenons un objet ID2D1DeviceContext2 qui permet la création et le dessin de géométrie, ainsi que des fonctionnalités telles que l’encre et le rendu de maillage de gradient. Nous créons ensuite une série de pinceaux colorés à l’aide de l’objet ID2D1SolidColorBrush pour dessiner les éléments suivants de l’interface utilisateur.

  • Pinceau noir pour les arrière-plans des rectangles
  • Pinceau blanc pour le texte d’état
  • Pinceau orange pour le texte d’action

DeviceResources::SetDpi

La méthode DeviceResources::SetDpi définit le nombre de points par pouce de la fenêtre. Cette méthode est appelée lorsque le DPI est modifié et doit être réajusté, ce qui se produit lorsque la fenêtre du jeu est redimensionnée. Après avoir mis à jour le DPI, cette méthode appelle égalementDeviceResources::CreateWindowSizeDependentResources pour s’assurer que les ressources nécessaires sont recréées à chaque fois que la fenêtre est redimensionnée.

GameInfoOverlay::CreateWindowsSizeDependentResources

La méthode GameInfoOverlay::CreateWindowsSizeDependentResources est l’endroit où tous nos dessins sont réalisés. Voici un aperçu des étapes de la méthode.

  • Trois rectangles sont créés pour délimiter le texte de l’interface utilisateur pour le titre, le corps et le texte d’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
        );
    
  • Une image bitmap est créée sous le nom de m_levelBitmap, en tenant compte du DPI actuel à l’aide de CreateBitmap.

  • m_levelBitmap est défini comme notre cible de rendu 2D à l’aide de ID2D1DeviceContext::SetTarget.

  • Le Bitmap est nettoyé et chaque pixel est rendu noir à l’aide de ID2D1RenderTarget::Clear.

  • ID2D1RenderTarget::BeginDraw est appelé pour lancer le dessin.

  • DrawText est appelé pour dessiner le texte stocké dans m_titleString, m_bodyString et m_actionString dans le rectangle approprié en utilisant le ID2D1SolidColorBrush correspondant.

  • ID2D1RenderTarget::EndDraw est appelé pour arrêter toutes les opérations de dessin sur m_levelBitmap.

  • Une autre image bitmap est créée à l’aide de CreateBitmap nommée m_tooSmallBitmap pour servir de repli, ne s’affichant que si la configuration de l’écran est trop petite pour le jeu.

  • Répétez le processus de dessin sur m_levelBitmap pour m_tooSmallBitmap, en ne dessinant cette fois que la chaîne Paused dans le corps.

Il ne nous manque plus que six méthodes pour remplir le texte de nos six états de superposition !

Représentation de l’état du jeu

Chacun des six états de superposition du jeu possède une méthode correspondante dans l’objet GameInfoOverlay. Ces méthodes dessinent une variante de la superposition pour communiquer au joueur des informations explicites sur le jeu lui-même. Cette communication est représentée par une chaîne de caractères Title et Body. Comme l’exemple a déjà configuré les ressources et la mise en page pour ces informations lors de son initialisation et avec la méthode GameInfoOverlay::CreateDeviceDependentResources, il suffit de fournir les chaînes spécifiques à l’état de l’incrustation.

L’état de la superposition est défini par un appel à l’une des méthodes suivantes.

État du jeu Méthode Status set Champs d’état
Chargement GameInfoOverlay::SetGameLoading Title
Loading Resources
Body
Impression incrémentielle de "." pour indiquer l’activité de chargement.
Stats initiales de début/de haut score GameInfoOverlay::SetGameStats Titre
Score le plus élevé
Texte
nombre de niveaux complétés
Total de points
nombre de coups tirés
Début du niveau GameInfoOverlay::SetLevelStart Titre
Niveau
Corps
Description de l’objectif du niveau.
Jeu en pause GameInfoOverlay::SetPause Titre
jeu en pause
texte
aucun
Fin du jeu GameInfoOverlay::SetGameOver Titre
Game Over
texte
Nombre de niveaux terminés
Total de points
Nombre de coups tirés
Nombre de niveaux terminés
score le plus élevé
Partie gagnée GameInfoOverlay::SetGameOver Titre
Vous avez GAGNÉ!
texte
nombre de niveaux terminés
Total de points
nombre de coups tirés
nombre de niveaux terminés
score le plus élevé

Avec la méthode GameInfoOverlay::CreateWindowSizeDependentResources, l’exemple a déclaré trois zones rectangulaires qui correspondent à des régions spécifiques de la superposition.

En gardant ces zones à l’esprit, examinons l’une des méthodes spécifiques à l’état, GameInfoOverlay::SetGameStats, et voyons comment l’incrustation est dessinée.

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

En utilisant le contexte de l’appareil Direct2D initialisé par l’objet GameInfoOverlay, cette méthode remplit les rectangles du titre et du corps en noir à l’aide de la brosse d’arrière-plan. Elle dessine le texte de la chaîne "High Score" dans le rectangle de titre et une chaîne contenant les informations de mise à jour de l’état du jeu dans le rectangle de corps à l’aide de la brosse de texte blanche.

Le rectangle d’action est mis à jour par un appel ultérieur à GameInfoOverlay::SetAction à partir d’une méthode de l’objet GameMain, qui fournit les informations sur l’état du jeu dont GameInfoOverlay::SetAction a besoin pour déterminer le bon message à adresser au joueur, tel que "Appuyez pour continuer".

La superposition pour un état donné est choisie dans la méthode GameMain::SetGameInfoOverlay comme ceci :

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

Le jeu dispose désormais d’un moyen de communiquer des informations textuelles au joueur en fonction de l’état du jeu, et nous disposons d’un moyen de modifier ce qui est affiché au joueur tout au long du jeu.

Étapes suivantes

Dans la rubrique suivante, Ajouter des contrôles, nous verrons comment le joueur interagit avec l’exemple de jeu et comment la saisie modifie l’état du jeu.