Définir l’objet jeu principal
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.
Une fois que vous avez mis en place l’infrastructure de base de l’exemple de jeu et implémenté un ordinateur d’état qui gère les comportements utilisateur et système de haut niveau, vous souhaiterez examiner les règles et les mécanismes qui transforment l’exemple de jeu en jeu. Examinons les détails de l’objet principal de l’exemple de jeu et comment traduire des règles de jeu en interactions avec le monde du jeu.
Objectifs
- Découvrez comment appliquer des techniques de développement de base pour implémenter des règles de jeu et des mécanismes pour un jeu DirectX UWP.
Objet de jeu principal
Dans l’exemple de jeu Simple3DGameDX , Simple3DGame est la classe d’objets de jeu principale. Une instance de Simple3DGame est construite, indirectement, via la méthode App ::Load .
Voici quelques-unes des fonctionnalités de la classe Simple3DGame .
- Contient l’implémentation de la logique de jeu.
- Contient des méthodes qui communiquent ces détails.
- Modifications de l’état du jeu à l’ordinateur d’état défini dans l’infrastructure d’application.
- Change dans l’état du jeu de l’application à l’objet de jeu lui-même.
- Détails de la mise à jour de l’interface utilisateur du jeu (superposition et affichage tête haute), animations et physiques (dynamique).
Remarque
La mise à jour des graphiques est gérée par la classe GameRenderer , qui contient des méthodes permettant d’obtenir et d’utiliser des ressources d’appareil graphique utilisées par le jeu. Pour plus d’informations, consultez l’infrastructure de rendu I : Présentation du rendu.
- Sert de conteneur pour les données qui définissent une session de jeu, un niveau ou une durée de vie, selon la façon dont vous définissez votre jeu à un niveau élevé. Dans ce cas, les données d’état du jeu concernent la durée de vie du jeu et sont initialisées une fois lorsqu’un utilisateur lance le jeu.
Pour afficher les méthodes et les données définies par cette classe, consultez la classe Simple3DGame ci-dessous.
Initialiser et démarrer le jeu
Lorsqu’un joueur démarre le jeu, l’objet de jeu doit initialiser son état, créer et ajouter la superposition, définir les variables qui suivent les performances du joueur et instancier les objets qu’il utilisera pour générer les niveaux. Dans cet exemple, cette opération est effectuée lorsque l’instance GameMain est créée dans App ::Load.
L’objet de jeu, de type Simple3DGame, est créé dans le constructeur GameMain ::GameMain . Il est ensuite initialisé à l’aide de la méthode Simple3DGame ::Initialize pendant le fire-and-forget GameMain ::ConstructInBackground fire-and-forget coroutine, qui est appelé à partir de GameMain ::GameMain.
Méthode Simple3DGame ::Initialize
L’exemple de jeu configure ces composants dans l’objet de jeu.
- Un nouvel objet de lecture audio est créé.
- Les tableaux des primitives graphiques du jeu sont créés, y compris des tableaux pour les primitives de niveau, les ammo et les obstacles.
- Un emplacement pour enregistrer les données d’état du jeu est créé, nommé Game et placé dans l’emplacement de stockage des paramètres de données de l’application spécifié par ApplicationData ::Current.
- Un minuteur de jeu et la bitmap de superposition initiale dans le jeu sont créés.
- Une nouvelle caméra est créée avec un ensemble spécifique de paramètres d’affichage et de projection.
- L’appareil d’entrée (le contrôleur) est défini sur la même hauteur de départ et le même lacet que la caméra, de sorte que le joueur a une correspondance de 1 à 1 entre la position de contrôle de départ et la position de la caméra.
- L’objet lecteur est créé et défini sur actif. Nous utilisons un objet sphère pour détecter la proximité du joueur aux murs et aux obstacles et pour empêcher la caméra de se placer dans une position susceptible de briser l’immersion.
- La primitive du monde du jeu est créée.
- Les obstacles de cylindre sont créés.
- Les cibles (objets Visage ) sont créées et numérotées.
- Les sphères d’ammo sont créées.
- Les niveaux sont créés.
- Le score élevé est chargé.
- Tout état de jeu enregistré antérieur est chargé.
Le jeu possède désormais des instances de tous les composants clés : le monde, le joueur, les obstacles, les cibles et les sphères d’ammo. Il a également des instances des niveaux, qui représentent des configurations de tous les composants ci-dessus et leurs comportements pour chaque niveau spécifique. Voyons maintenant comment le jeu génère les niveaux.
Créer et charger des niveaux de jeu
La plupart des gros travaux de construction de niveau sont effectués dans les Level[N].h/.cpp
fichiers trouvés dans le dossier GameLevels de l’exemple de solution. Étant donné qu’elle se concentre sur une implémentation très spécifique, nous ne les couvrirons pas ici. L’important est que le code de chaque niveau est exécuté en tant qu’objet Level[N] distinct. Si vous souhaitez étendre le jeu, vous pouvez créer un objet Level[N] qui prend un nombre attribué en tant que paramètre et place de manière aléatoire les obstacles et les cibles. Vous pouvez également charger des données de configuration au niveau du chargement à partir d’un fichier de ressources ou même d’Internet.
Définir le gameplay
À ce stade, nous avons tous les composants dont nous avons besoin pour développer le jeu. Les niveaux ont été construits en mémoire à partir des primitives et sont prêts pour que le joueur commence à interagir avec.
Les meilleurs jeux réagissent instantanément à l’entrée du joueur et fournissent des commentaires immédiats. C’est vrai pour n’importe quel type de jeu, des tireurs de première personne en temps réel à des jeux de stratégie réfléchis et basés sur le tour.
Méthode Simple3DGame ::RunGame
Alors qu’un niveau de jeu est en cours, le jeu est dans l’état Dynamics .
GameMain ::Update est la boucle de mise à jour principale qui met à jour l’état de l’application une fois par image, comme indiqué ci-dessous. La boucle de mise à jour appelle la méthode Simple3DGame ::RunGame pour gérer le travail si le jeu est dans l’état Dynamics .
// Updates the application state once per frame.
void GameMain::Update()
{
// The controller object has its own update loop.
m_controller->Update();
switch (m_updateState)
{
...
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)
{
...
Simple3DGame ::RunGame gère l’ensemble de données qui définit l’état actuel du jeu pour l’itération actuelle de la boucle de jeu.
Voici la logique de flux de jeu dans Simple3DGame ::RunGame.
- La méthode met à jour le minuteur qui compte les secondes jusqu’à ce que le niveau soit terminé et teste si le temps du niveau a expiré. C’est l’une des règles du jeu : quand le temps s’écoule, si toutes les cibles n’ont pas été abattues, alors c’est le jeu terminé.
- Si le temps est écoulé, la méthode définit l’état du jeu TimeExpired et retourne à la méthode Update dans le code précédent.
- Si le temps reste, le contrôleur move-look est interrogé pour obtenir une mise à jour de la position de la caméra ; plus précisément, une mise à jour de l’angle de la vue normale projetant à partir du plan de caméra (où le joueur regarde) et la distance que l’angle a déplacé depuis que le contrôleur a été interrogé en dernier.
- La caméra est mise à jour en fonction des nouvelles données du contrôleur move-look.
- La dynamique, ou les animations et comportements d’objets dans le monde du jeu indépendamment du contrôle du joueur, sont mis à jour. Dans cet exemple de jeu, la méthode Simple3DGame ::UpdateDynamics est appelée pour mettre à jour le mouvement des sphères d’ammo qui ont été déclenchées, l’animation des obstacles du pilier et le mouvement des cibles. Pour plus d’informations, consultez Mettre à jour le monde du jeu.
- La méthode vérifie si les critères de réussite d’un niveau ont été remplis. Si c’est le cas, il finalise le score pour le niveau et vérifie s’il s’agit du dernier niveau (de 6). S’il s’agit du dernier niveau, la méthode retourne l’état du jeu GameState ::GameComplete ; sinon, elle renvoie l’état du jeu GameState ::LevelComplete .
- Si le niveau n’est pas terminé, la méthode définit l’état du jeu sur GameState ::Active et retourne.
Mettre à jour le monde du jeu
Dans cet exemple, lorsque le jeu est en cours d’exécution, la méthode Simple3DGame ::UpdateDynamics est appelée à partir de la méthode Simple3DGame ::RunGame (appelée à partir de GameMain ::Update) pour mettre à jour les objets qui sont rendus dans une scène de jeu.
Une boucle telle que UpdateDynamics appelle toutes les méthodes utilisées pour définir le monde du jeu en mouvement, indépendamment de l’entrée du joueur, pour créer une expérience de jeu immersive et rendre le niveau vivant. Cela inclut les graphiques qui doivent être rendus et l’exécution de boucles d’animation pour apporter un monde dynamique même s’il n’y a pas d’entrée de lecteur. Dans votre jeu, qui pourrait inclure des arbres se balançant dans le vent, des vagues cressant le long des lignes côtières, le tabagisme des machines et les monstres extraterrestres étirement et se déplaçant autour. Il englobe également l’interaction entre les objets, y compris les collisions entre la sphère du joueur et le monde, ou entre les munitions et les obstacles et les cibles.
Sauf lorsque le jeu est spécifiquement suspendu, la boucle de jeu doit continuer à mettre à jour le monde du jeu ; s’il s’agit d’une logique de jeu, d’algorithmes physiques ou d’un simple hasard.
Dans l’exemple de jeu, ce principe est appelé dynamique, et il englobe la montée et la chute des obstacles piliers, et le mouvement et les comportements physiques des sphères d’ammo lorsqu’ils sont déclenchés et en mouvement.
La méthode Simple3DGame ::UpdateDynamics
Cette méthode traite ces quatre ensembles de calculs.
- Les positions des sphères de munitions déclenchées dans le monde.
- Animation des obstacles du pilier.
- Intersection du joueur et des limites du monde.
- Les collisions entre les sphères de munitions et les obstacles, les cibles, d’autres sphères d’ammo et le monde.
L’animation des obstacles se produit dans une boucle définie dans les fichiers de code source Animate.h/.cpp . Le comportement des munitions et de toutes les collisions sont définis par des algorithmes de physique simplifiés, fournis dans le code et paramétrés par un ensemble de constantes globales pour le monde du jeu, y compris la gravité et les propriétés matérielles. Cela est calculé dans l’espace de coordonnées du monde du jeu.
Passer en revue le flux
Maintenant que nous avons mis à jour tous les objets de la scène et calculé les collisions, nous devons utiliser ces informations pour dessiner les modifications visuelles correspondantes.
Une fois GameMain ::Update terminée l’itération actuelle de la boucle de jeu, l’exemple appelle immédiatement GameRenderer ::Render pour prendre les données d’objet mises à jour et générer une nouvelle scène à présenter au joueur, comme indiqué ci-dessous.
void GameMain::Run()
{
while (!m_windowClosed)
{
if (m_visible)
{
switch (m_updateState)
{
case UpdateEngineState::Deactivated:
case UpdateEngineState::TooSmall:
...
// Otherwise, fall through and do normal processing to perform rendering.
default:
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
CoreProcessEventsOption::ProcessAllIfPresent);
// GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
// state, uses Simple3DGame::UpdateDynamics to update game world.
Update();
// Render is called immediately after the Update loop.
m_renderer->Render();
m_deviceResources->Present();
m_renderNeeded = false;
}
}
else
{
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
CoreProcessEventsOption::ProcessOneAndAllPending);
}
}
m_game->OnSuspending(); // Exiting due to window close, so save state.
}
Afficher les graphismes du monde du jeu
Nous recommandons que les graphismes d’une mise à jour de jeu se mettent souvent à jour, idéalement aussi souvent que la boucle principale du jeu itère. Comme la boucle itère, l’état du monde du jeu est mis à jour, avec ou sans entrée de joueur. Cela permet aux animations calculées et aux comportements d’être affichés en douceur. Imaginez si nous avions une scène simple d’eau qui s’est déplacée uniquement lorsque le joueur a appuyé sur un bouton. Cela ne serait pas réaliste ; un bon jeu semble lisse et fluide tout le temps.
Rappelez-vous la boucle de l’exemple de jeu, comme indiqué ci-dessus dans GameMain ::Run. Si la fenêtre principale du jeu est visible et n’est pas alignée ou désactivée, le jeu continue à mettre à jour et à afficher les résultats de cette mise à jour. La méthode GameRenderer ::Render que nous examinons ensuite affiche une représentation de cet état. Cette opération est effectuée immédiatement après un appel à GameMain ::Update, qui inclut Simple3DGame ::RunGame pour mettre à jour les états, comme indiqué dans la section précédente.
GameRenderer ::Render dessine la projection du monde 3D, puis dessine la superposition Direct2D sur celle-ci. Une fois terminé, il présente la chaîne d’échange finale avec les mémoires tampons combinées pour l’affichage.
Remarque
Il existe deux états pour la superposition Direct2D de l’exemple de jeu : l’un où le jeu affiche la superposition d’informations de jeu qui contient la bitmap pour le menu pause, et l’autre où le jeu affiche les points croisés ainsi que les rectangles pour le contrôleur de déplacement tactile. Le texte du score est dessiné dans les deux états. Pour plus d’informations, consultez Infrastructure de rendu I : présentation du rendu.
La méthode GameRenderer ::Render
void GameRenderer::Render()
{
bool stereoEnabled{ m_deviceResources->GetStereoState() };
auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };
...
if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
{
// This section is only used after the game state has been initialized and all device
// resources needed for the game have been created and associated with the game objects.
...
for (auto&& object : m_game->RenderObjects())
{
object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
}
}
d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
d2dContext->BeginDraw();
// To handle the swapchain being pre-rotated, set the D2D transformation to include it.
d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());
if (m_game != nullptr && m_gameResourcesLoaded)
{
// This is only used after the game state has been initialized.
m_gameHud.Render(m_game);
}
if (m_gameInfoOverlay.Visible())
{
d2dContext->DrawBitmap(
m_gameInfoOverlay.Bitmap(),
m_gameInfoOverlayRect
);
}
...
}
}
La classe Simple3DGame
Il s’agit des méthodes et des membres de données définis par la classe Simple3DGame .
Fonctions Membre
Les fonctions membres publiques définies par Simple3DGame incluent celles ci-dessous.
- Initialiser. Définit les valeurs de départ des variables globales et initialise les objets de jeu. Cela est abordé dans la section Initialiser et démarrer le jeu .
- LoadGame. Initialise un nouveau niveau et commence à le charger.
- LoadLevelAsync. Coroutine qui initialise le niveau, puis appelle une autre coroutine sur le renderer pour charger les ressources de niveau spécifique à l’appareil. Cette méthode s’exécute dans un thread distinct ; par conséquent, seules les méthodes ID3D11Device (par opposition aux méthodes ID3D11DeviceContext) peuvent être appelées à partir de ce thread. Toutes les méthodes de contexte d’appareil sont appelées dans la méthode FinaliseLoadLevel . Si vous débutez avec la programmation asynchrone, consultez Concurrence et opérations asynchrones avec C++/WinRT.
- FinaliseLoadLevel. Termine tout travail pour le chargement au niveau qui doit être effectué sur le thread principal. Cela inclut tous les appels aux méthodes de contexte d’appareil Direct3D 11 (ID3D11DeviceContext).
- StartLevel. Démarre le gameplay pour un nouveau niveau.
- PauseGame. Suspend le jeu.
- RunGame. Exécute une itération de la boucle de jeu. Elle est appelée à partir d’App ::Update une fois chaque itération de la boucle de jeu si l’état du jeu est Actif.
- OnSuspending et OnResuming. Suspendre/reprendre l’audio du jeu, respectivement.
Voici les fonctions membres privées.
- LoadSavedState et SaveState. Chargez/enregistrez l’état actuel du jeu, respectivement.
- LoadHighScore et SaveHighScore. Chargez/enregistrez le score élevé entre les jeux, respectivement.
- InitializeAmmo. Réinitialise l’état de chaque objet sphère utilisé comme munitions à son état d’origine pour le début de chaque tour.
- UpdateDynamics. Il s’agit d’une méthode importante, car elle met à jour tous les objets de jeu basés sur des routines d’animation en boîte, la physique et l’entrée de contrôle. C’est le cœur de l’interactivité qui définit le jeu. Ceci est abordé dans la section Mettre à jour le monde du jeu.
Les autres méthodes publiques sont l’accesseur de propriété qui retourne des informations spécifiques au jeu et à la superposition à l’infrastructure d’application pour l’affichage.
Membres de données
Ces objets sont mis à jour au fur et à mesure que la boucle de jeu s’exécute.
- Objet MoveLookController . Représente l’entrée du lecteur. Pour plus d’informations, consultez Ajout de contrôles.
- Objet GameRenderer . Représente un renderer Direct3D 11, qui gère tous les objets spécifiques à l’appareil et leur rendu. Pour plus d’informations, consultez l’infrastructure de rendu I.
- Objet audio . Contrôle la lecture audio du jeu. Pour plus d’informations, consultez Ajout de son.
Le reste des variables de jeu contiennent les listes des primitives, ainsi que leurs quantités dans le jeu respectives, ainsi que les données et contraintes spécifiques au jeu.
Étapes suivantes
Nous n’avons pas encore parlé du moteur de rendu réel : comment les appels aux méthodes de rendu sur les primitives mises à jour sont transformés en pixels sur votre écran. Ces aspects sont abordés en deux parties : Infrastructure de rendu I : Présentation du rendu et de l’infrastructure de rendu II : Rendu de jeu. Si vous êtes plus intéressé par la façon dont les contrôles de joueur mettent à jour l’état du jeu, consultez Ajout de contrôles.