Gestion de flux de jeu

Notes

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

Le jeu a maintenant une fenêtre, a inscrit certains gestionnaires d’événements et a chargé des ressources de manière asynchrone. Cette rubrique explique l’utilisation des états de jeu, comment gérer des états de jeu clés spécifiques et comment créer une boucle de mise à jour pour le moteur de jeu. Ensuite, nous allons découvrir le flux d’interface utilisateur et, enfin, en savoir plus sur les gestionnaires d’événements nécessaires pour un jeu UWP.

États de jeu utilisés pour gérer le flux de jeu

Nous utilisons les états du jeu pour gérer le flux du jeu.

Lorsque l’exemple de jeu Simple3DGameDX s’exécute pour la première fois sur une machine, il est dans un état où aucun jeu n’a été démarré. Par la suite, le jeu peut se trouver dans l’un de ces états.

  • Aucun jeu n’a été démarré, ou le jeu se trouve entre les niveaux (le score élevé est égal à zéro).
  • La boucle de jeu est en cours d’exécution et se trouve au milieu d’un niveau.
  • La boucle de jeu n’est pas en cours d’exécution car un jeu a été terminé (le score élevé a une valeur différente de zéro).

Votre jeu peut avoir autant d’états qu’il en a besoin. Mais n’oubliez pas qu’il peut être arrêté à tout moment. Et quand il reprend, l’utilisateur s’attend à ce qu’il reprenne dans l’état dans lequel il était quand il a été arrêté.

Gestion de l’état du jeu

Ainsi, lors de l’initialisation du jeu, vous devrez prendre en charge le démarrage à froid du jeu ainsi que la reprise du jeu après l’avoir arrêté en vol. L’exemple Simple3DGameDX enregistre toujours son état de jeu afin de donner l’impression qu’il ne s’est jamais arrêté.

En réponse à un événement de suspension, le jeu est suspendu, mais les ressources du jeu sont toujours en mémoire. De même, l’événement de reprise est géré pour s’assurer que l’exemple de jeu reprend dans l’état dans lequel il se trouvait lorsqu’il a été suspendu ou terminé. Selon l’état, différentes options sont présentées au joueur.

  • Si le jeu reprend à mi-niveau, il apparaît suspendu, et la superposition offre la possibilité de continuer.
  • Si le jeu reprend dans un état où le jeu est terminé, il affiche les scores élevés et une option pour jouer à un nouveau jeu.
  • Enfin, si le jeu reprend avant qu’un niveau n’ait commencé, la superposition présente une option de démarrage à l’utilisateur.

L’exemple de jeu ne distingue pas si le jeu démarre à froid, lance pour la première fois sans événement de suspension ou reprend à partir d’un état suspendu. Il s’agit de la conception appropriée pour toute application UWP.

Dans cet exemple, l’initialisation des états de jeu se produit dans GameMain::InitializeGameState (un plan de cette méthode est affiché dans la section suivante).

Voici un organigramme pour vous aider à visualiser le flux. Il couvre à la fois l’initialisation et la boucle de mise à jour.

  • L’initialisation commence au nœud Démarrer lorsque vous case activée pour l’état actuel du jeu. Pour le code du jeu, consultez GameMain::InitializeGameState dans la section suivante.

Machine à états principale de notre jeu

Méthode GameMain::InitializeGameState

La méthode GameMain::InitializeGameState est appelée indirectement via le constructeur de la classe GameMain, ce qui est le résultat de la création d’une instance GameMain dans App::Load.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

Mettre à jour le moteur de jeu

La méthode App::Run appelle GameMain::Run. GameMain::Run est une machine à états de base pour gérer toutes les actions majeures qu’un utilisateur peut effectuer. Le niveau le plus élevé de cette machine d’état concerne le chargement d’un jeu, le jeu d’un niveau spécifique ou la poursuite d’un niveau après que le jeu a été suspendu (par le système ou par l’utilisateur).

Dans l’exemple de jeu, le jeu peut se trouver dans 3 états principaux (représentés par l’énumération UpdateEngineState ).

  1. UpdateEngineState::WaitingForResources. La boucle de jeu effectue une itération, incapable de procéder à la transition tant que les ressources (en particulier, les ressources graphiques) ne sont pas disponibles. Une fois les tâches de chargement de ressources asynchrones terminées, nous mettons à jour l’état sur UpdateEngineState::ResourcesLoaded. Cela se produit généralement entre les niveaux lorsque le niveau charge de nouvelles ressources à partir d’un disque, d’un serveur de jeu ou d’un back-end cloud. Dans l’exemple de jeu, nous simulons ce comportement, car l’exemple n’a pas besoin de ressources supplémentaires par niveau à ce moment-là.
  2. UpdateEngineState::WaitingForPress. La boucle de jeu effectue une itération, en attente d’une entrée utilisateur spécifique. Cette entrée est une action du joueur permettant de charger un jeu, de démarrer un niveau ou de continuer un niveau. L’exemple de code fait référence à ces sous-états via l’énumération PressResultState .
  3. UpdateEngineState::D ynamics. La boucle de jeu est en cours d’exécution et l’utilisateur joue. Pendant que l’utilisateur joue, le jeu recherche 3 conditions sur lesquelles il peut effectuer la transition :
  • GameState::TimeExpired. Expiration de la limite de temps pour un niveau.
  • GameState::LevelComplete. Achèvement d’un niveau par le joueur.
  • GameState::GameComplete. Achèvement de tous les niveaux par le joueur.

Un jeu est simplement une machine à états contenant plusieurs machines d’état plus petites. Chaque état spécifique doit être défini par des critères très spécifiques. Les transitions d’un état à un autre doivent être basées sur une entrée utilisateur discrète ou sur des actions système (telles que le chargement des ressources graphiques).

Lors de la planification de votre jeu, envisagez d’extraire l’intégralité du flux de jeu pour vous assurer que vous avez traité toutes les actions possibles que l’utilisateur ou le système peut effectuer. Un jeu peut être très compliqué, donc une machine à états est un outil puissant pour vous aider à visualiser cette complexité et à la rendre plus facile à gérer.

Examinons le code de la boucle de mise à jour.

La méthode GameMain::Update

Il s’agit de la structure de la machine à états utilisée pour mettre à jour le moteur de jeu.

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

Mettre à jour l’interface utilisateur

Nous devons tenir le joueur informé de l’état du système et permettre à l’état du jeu de changer en fonction des actions du joueur et des règles qui définissent le jeu. De nombreux jeux, y compris cet exemple de jeu, utilisent généralement des éléments d’interface utilisateur pour présenter ces informations au joueur. L’interface utilisateur contient des représentations de l’état du jeu et d’autres informations spécifiques au jeu, telles que le score, les munitions ou le nombre de chances restantes. L’interface utilisateur est également appelée superposition, car elle est rendue séparément du pipeline graphique main et placée au-dessus de la projection 3D.

Certaines informations d’interface utilisateur sont également présentées sous la forme d’un affichage tête haute (HUD) pour permettre à l’utilisateur de voir ces informations sans se détacher complètement de la zone de jeu main. Dans l’exemple de jeu, nous créons cette superposition à l’aide des API Direct2D. Nous pourrions également créer cette superposition à l’aide de XAML, que nous aborderons dans Extension de l’exemple de jeu.

L’interface utilisateur comporte deux composants.

  • HUD qui contient le score et des informations sur l’état actuel du jeu.
  • La bitmap de pause, qui est un rectangle noir avec un texte superposé lorsque le jeu est dans l’état de pause/suspension. Il s’agit de la superposition du jeu. Nous en parlons plus tard dans Ajout d’une interface utilisateur.

Rien d’étonnant à cela, la superposition a également une machine à états. La superposition peut afficher un début de niveau ou un message de basculement. Il s’agit essentiellement d’un canevas sur lequel nous pouvons générer des informations sur l’état du jeu que nous voulons afficher au joueur pendant que le jeu est suspendu ou suspendu.

La superposition rendue peut être l’un de ces six écrans, selon l’état du jeu.

  1. Écran de progression du chargement des ressources au début du jeu.
  2. Écran des statistiques de jeu.
  3. Écran de message de début de niveau.
  4. Écran de basculement lorsque tous les niveaux sont terminés sans que le temps soit imparti.
  5. Écran de basculement lorsque le temps est dépassé.
  6. Écran de menu Pause.

Séparer votre interface utilisateur du pipeline graphique de votre jeu vous permet de travailler dessus indépendamment du moteur de rendu graphique du jeu, et réduit considérablement la complexité du code de votre jeu.

Voici comment l’exemple de jeu structure la machine d’état de la superposition.

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

Gestion des événements

Comme nous l’avons vu dans la rubrique Définir l’infrastructure d’application UWP du jeu , de nombreuses méthodes de fournisseur d’affichage de la classe App inscrivent des gestionnaires d’événements. Ces méthodes doivent gérer correctement ces événements importants avant d’ajouter des mécanismes de jeu ou de démarrer le développement graphique.

La gestion correcte des événements en question est fondamentale pour l’expérience de l’application UWP. Étant donné qu’une application UWP peut à tout moment être activée, désactivée, redimensionnée, ancrée, non mappée, suspendue ou reprise, le jeu doit s’inscrire à ces événements dès qu’il le peut, et les gérer de manière à maintenir l’expérience fluide et prévisible pour le joueur.

Il s’agit des gestionnaires d’événements utilisés dans cet exemple et des événements qu’ils gèrent.

Gestionnaire d'événements Description
OnActivated Gère CoreApplicationView::Activated. L’application de jeu ayant été amenée au premier plan, la fenêtre principale est activée.
OnDpiChanged Gère Graphics::D isplay::D isplayInformation::D piChanged. La PPP de l’affichage a changé et le jeu ajuste ses ressources en conséquence.
Remarque Les coordonnées CoreWindow sont exprimées en pixels indépendants de l’appareil (DIPs) pour Direct2D. Par conséquent, vous devez indiquer à Direct2D la modification des PPP afin d’afficher correctement les primitives ou composants 2D.
OnOrientationChanged Gère Graphics::D isplay::D isplayInformation::OrientationChanged. L’orientation des modifications d’affichage et du rendu doit être mise à jour.
OnDisplayContentsInvalidated Gère Graphics::D isplay::D isplayInformation::D isplayContentsInvalidated. L’affichage nécessite un redessinage et votre jeu doit être rendu à nouveau.
OnResuming Gère CoreApplication::Resuming. L’application de jeu restaure le jeu qui est dans un état suspendu.
OnSuspending Gère CoreApplication::Suspending. L’application de jeu enregistre son état sur disque. Elle dispose de 5 secondes pour enregistrer l’état dans le dispositif de stockage.
OnVisibilityChanged Gère CoreWindow::VisibilityChanged. L’application de jeu a modifié la visibilité : elle est devenue soit visible, soit invisible car une autre application est devenue visible.
OnWindowActivationChanged Gère CoreWindow::Activated. La fenêtre principale de l’application de jeu ayant été activée ou désactivée, elle doit supprimer le focus et interrompre le jeu, ou regagner le focus. Dans les deux cas, la superposition indique que le jeu est suspendu.
OnWindowClosed Gère CoreWindow::Closed. L’application de jeu ferme la fenêtre principale et suspend le jeu.
OnWindowSizeChanged Gère CoreWindow::SizeChanged. L’application de jeu réaffecte les ressources graphiques et la superposition pour tenir compte de la modification de la taille, puis met à jour la cible de rendu.

Étapes suivantes

Dans cette rubrique, nous avons vu comment le flux de jeu global est géré à l’aide des états de jeu, et qu’un jeu est constitué de plusieurs machines d’état différentes. Nous avons également vu comment mettre à jour l’interface utilisateur et gérer les gestionnaires d’événements d’application clés. Nous sommes maintenant prêts à plonger dans la boucle de rendu, le jeu et ses mécanismes.

Vous pouvez parcourir les autres rubriques qui documentent ce jeu dans n’importe quel ordre.