Gestion de flux de jeu
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.
Le jeu a maintenant une fenêtre, a enregistré 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 des états de 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 s’exécute, il peut se trouver dans l’un de ces états.
- Aucun jeu n’a été démarré, ou le jeu est 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 en raison d’un jeu ayant été terminé (le score élevé a une valeur non nulle).
Votre jeu peut avoir autant d’états que nécessaire. Mais n’oubliez pas qu’il peut être arrêté à tout moment. Et quand il reprend, l’utilisateur s’attend à ce qu’il reprend dans l’état dans lequel il était terminé.
Gestion de l’état du jeu
Par conséquent, pendant l’initialisation du jeu, vous devrez prendre en charge le démarrage froid du jeu, ainsi que reprendre le 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 n’a jamais cessé.
En réponse à un événement de suspension, le gameplay 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 récupère dans l’état dans lequel il était suspendu ou arrêté. 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 le début d’un niveau, 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 n’importe quelle 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 recherchez l’état actuel du jeu. Pour le code du jeu, consultez GameMain ::InitializeGameState dans la section suivante.
- Pour plus d’informations sur la boucle de mise à jour, accédez au moteur de jeu Update. Pour le code de jeu, accédez à GameMain ::Update.
La méthode GameMain ::InitializeGameState
La méthode GameMain ::InitializeGameState est appelée indirectement par le biais du constructeur de la classe GameMain , 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. Dans GameMain ::Run est un ordinateur d’état de base permettant de gérer toutes les actions principales qu’un utilisateur peut entreprendre. Le niveau le plus élevé de cette machine d’état traite du chargement d’un jeu, du jeu d’un niveau spécifique ou de la poursuite d’un niveau une fois le jeu suspendu (par le système ou par l’utilisateur).
Dans l’exemple de jeu, il existe 3 états principaux (représentés par l’énumération UpdateEngineState ) dans utilisant le jeu.
- UpdateEngineState ::WaitingForResources. La boucle de jeu est en vélo, incapable de passer jusqu’à ce que les ressources (en particulier les ressources graphiques) soient 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 du disque, d’un serveur de jeux ou d’un serveur principal 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à.
- UpdateEngineState ::WaitingForPress. La boucle de jeu est cycliste, en attente d’une entrée utilisateur spécifique. Cette entrée est une action de joueur pour charger un jeu, démarrer un niveau ou continuer un niveau. L’exemple de code fait référence à ces sous-états via l’énumération PressResultState .
- UpdateEngineState ::D ynamics. La boucle de jeu s’exécute avec l’utilisateur jouant. Pendant que l’utilisateur joue, le jeu vérifie 3 conditions sur lesquelles il peut passer :
- 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 un ordinateur d’état 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 des actions système (telles que le chargement de ressources graphiques).
Lors de la planification de votre jeu, envisagez de tirer l’intégralité du flux de jeu pour vous assurer que vous avez résolu toutes les actions possibles que l’utilisateur ou le système peut effectuer. Un jeu peut être très compliqué, donc un ordinateur d’état est un outil puissant pour vous aider à visualiser cette complexité et à le rendre plus gérable.
Examinons le code de la boucle de mise à jour.
Méthode GameMain ::Update
Il s’agit de la structure de l’ordinateur d’état utilisé 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 garder 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, l’ammo ou le nombre de chances restantes. L’interface utilisateur est également appelée superposition, car elle est rendue séparément du pipeline graphique principal et placée sur la projection 3D.
Certaines informations d’interface utilisateur sont également présentées sous la forme d’un affichage tête-haut (HUD) pour permettre à l’utilisateur de voir ces informations sans prendre leurs yeux entièrement hors de la zone de jeu principale. 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 abordons dans l’extension de l’exemple de jeu.
Il existe deux composants à l’interface utilisateur.
- HUD qui contient le score et les informations sur l’état actuel du jeu.
- Bitmap de pause, qui est un rectangle noir avec du texte superposé pendant l’état suspendu/suspendu du jeu. C’est la superposition de jeu. Nous l’avons abordé plus en détail dans l’ajout d’une interface utilisateur.
Sans surprise, la superposition a également une machine d’état. La superposition peut afficher un début de niveau ou un message de jeu. 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, en fonction de l’état du jeu.
- Écran de progression du chargement des ressources au début du jeu.
- Écran des statistiques de gameplay.
- Écran du message de démarrage de niveau.
- Écran de jeu lorsque tous les niveaux sont terminés sans délai d’attente.
- Écran de jeu lorsque le temps s’est écoulé.
- Écran du menu Suspendre.
La séparation de votre interface utilisateur du pipeline graphique de votre jeu vous permet de travailler 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 l’ordinateur 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, la plupart des 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 appropriée 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, alignée, annulé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 a été apportée au premier plan, de sorte que la fenêtre principale est activée. |
OnDpiChanged | Gère Graphics ::D isplay ::D isplayInformation ::D piChanged. Le ppp de l’affichage a changé et le jeu ajuste ses ressources en conséquence.
Notez queles coordonnées CoreWindow sont en pixels indépendants de l’appareil (DIPs) pour Direct2D. Par conséquent, vous devez notifier Direct2D de la modification de ppp pour afficher correctement les ressources ou primitives 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 ::Reprise. L’application de jeu restaure le jeu à partir d’un état suspendu. |
OnSuspending | Gère CoreApplication ::Suspending. L’application de jeu enregistre son état sur le disque. Il a 5 secondes pour enregistrer l’état dans le stockage. |
OnVisibilityChanged | Gère CoreWindow ::VisibilityChanged. L’application de jeu a changé de visibilité et est devenue visible ou invisible par une autre application. |
OnWindowActivationChanged | Gère CoreWindow ::Activated. La fenêtre principale de l’application de jeu a été désactivée ou activée. Elle doit donc supprimer le focus et suspendre le jeu, ou reprendre 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éalloue les ressources graphiques et la superposition pour prendre en charge le changement de 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. Maintenant, nous sommes prêts à plonger dans la boucle de rendu, le jeu et ses mécanismes.
Vous pouvez parcourir les rubriques restantes qui documentent ce jeu dans n’importe quel ordre.