Définir l’objet jeu principal

Notes

Cette rubrique fait partie de la série de tutoriels 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.

Une fois que vous avez mis en place l’infrastructure de base de l’exemple de jeu et implémenté une machine à états qui gère les comportements des utilisateurs et des systèmes de haut niveau, vous devez examiner les règles et les mécanismes qui transforment l’exemple de jeu en un jeu. Examinons les détails de l’objet main de l’exemple de jeu et comment traduire les règles du 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’objet de jeu main. Un instance de Simple3DGame est construit, 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 sur la machine d’état définie dans l’infrastructure d’application.
    • Modifications de 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), des animations et de la physique (la dynamique).

    Notes

    La mise à jour des graphiques est gérée par la classe GameRenderer , qui contient des méthodes permettant d’obtenir et d’utiliser les ressources d’appareil graphique utilisées par le jeu. Pour plus d’informations, consultez Infrastructure de rendu I : Introduction au 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 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 qui seront utilisés pour générer les niveaux. Dans cet exemple, cette opération est effectuée lorsque le instance GameMain est créé 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 la coroutine fire-and-forget GameMain::ConstructInBackground , qui est appelée à partir de GameMain::GameMain.

Méthode Simple3DGame::Initialize

L’exemple de jeu configure ces composants dans l’objet de jeu.

  • Un nouvel objet lecture audio est créé.
  • Des tableaux pour les primitives graphiques du jeu sont créés, notamment des tableaux pour les primitives de niveau, les munitions et les obstacles.
  • Un emplacement pour enregistrer les données de l’état du jeu est créé : il est nommé Game et est placé dans l’emplacement de stockage des paramètres des données d’application spécifié par ApplicationData::Current.
  • Un minuteur de jeu et la bitmap de superposition initiale, intégrée au jeu, sont créés.
  • Une nouvelle caméra est créée avec un ensemble spécifique de paramètres de vue et de projection.
  • Le périphérique d’entrée (contrôleur) étant défini sur les mêmes tangage et lacet de départ que la caméra, le joueur a une correspondance un-à-un entre la position du contrôle de départ et la position de la caméra.
  • L’objet joueur est créé et associé à l’état actif. Nous utilisons un objet sphère pour détecter la proximité du joueur avec les murs et les obstacles et pour empêcher la caméra d’être placée dans une position susceptible de briser l’immersion.
  • La primitive du monde du jeu est créée.
  • Les obstacles sous forme de cylindres sont créés.
  • Les cibles (objets Face) sont créées et numérotées.
  • Les sphères de munitions sont créées.
  • Les niveaux sont créés.
  • Les meilleurs scores sont chargés.
  • Tout état de jeu précédemment enregistré est chargé.

Le jeu a maintenant des instances de tous les composants clés : le monde, le joueur, les obstacles, les cibles et les sphères de munitions. Il a également des instances des niveaux, qui représentent les configurations de tous les composants ci-dessus ainsi que leurs comportements pour chaque niveau spécifique. Voyons maintenant comment le jeu crée les niveaux.

Générer et charger des niveaux de jeu

La plupart des tâches de construction de niveau sont effectuées dans les Level[N].h/.cpp fichiers trouvés dans le dossier GameLevels de l’exemple de solution. Étant donné qu’il 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 soit 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 lui faire charger des données de configuration au niveau 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 aux entrées des joueurs et fournissent des commentaires immédiats. Cela est vrai pour n’importe quel type de jeu, des jeux de tir à la première personne en temps réel aux jeux de stratégie au tour par tour réfléchis.

Méthode Simple3DGame::RunGame

Lorsqu’un niveau de jeu est en cours, le jeu est à l’état Dynamics .

GameMain::Update est la boucle de mise à jour main 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 à 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 l’heure du niveau a expiré. C’est l’une des règles du jeu : quand le temps est écoulé, si toutes les cibles n’ont pas été tirées, alors c’est le jeu terminé.
  • Si le temps est écoulé, la méthode définit l’état de 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 une mise à jour de la position de la caméra ; plus précisément, une mise à jour de l’angle de la projection normale de la vue à partir du plan de la caméra (où le joueur recherche), et la distance que cet angle a déplacée depuis que la manette a été interrogée en dernier.
  • La caméra est mise à jour en fonction des nouvelles données du contrôleur de déplacement/vue.
  • La dynamique, ou les animations et comportements des objets du monde du jeu indépendants 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 de munitions 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 du niveau et vérifie s’il s’agit du dernier niveau (sur 6). S’il s’agit du dernier niveau, la méthode retourne l’état de jeu GameState::GameComplete ; sinon, il retourne l’état de 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 mettre en mouvement le monde du jeu, indépendamment de l’entrée du joueur, afin de créer une expérience de jeu immersive et de rendre le niveau vivant. Cela inclut des graphiques qui doivent être rendus et des boucles d’animation en cours d’exécution pour créer un monde dynamique, même en l’absence d’entrée de lecteur. Dans votre jeu, cela peut inclure des arbres se balançant dans le vent, des vagues cressant le long des lignes de rivage, des machines fumant, et des monstres extraterrestres s’étirant 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 cibles.

Sauf lorsque le jeu est spécifiquement suspendu, la boucle de jeu doit continuer à mettre à jour le monde du jeu ; qu’il s’agisse d’une logique de jeu, d’algorithmes physiques ou d’un simple aléatoire.

Dans l’exemple de jeu, ce principe est appelé dynamique, et il englobe la montée et la chute des obstacles du pilier, ainsi que le mouvement et les comportements physiques des sphères de munitions lorsqu’elles sont déclenchées et en mouvement.

Méthode Simple3DGame::UpdateDynamics

Cette méthode traite ces quatre ensembles de calculs.

  • Les positions des sphères de munitions tirées dans le monde.
  • L’animation des colonnes d’obstacles.
  • L’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 de munitions et le monde.

L’animation des obstacles a lieu dans une boucle définie dans les fichiers de code source Animate.h/.cpp . Le comportement des munitions et 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. Tout 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 que GameMain::Update a terminé 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.
}

Restituer les graphismes du monde du jeu

Nous recommandons que les graphiques d’un jeu se mettent à jour souvent, idéalement exactement aussi souvent que la boucle de jeu main itère. À mesure que la boucle itère, l’état du monde du jeu est mis à jour, avec ou sans entrée de joueur. Cela permet d’afficher les animations et les comportements calculés en douceur. Imaginez si nous avions une simple scène d’eau qui se déplaçait uniquement lorsque le joueur appuyait sur un bouton. Ce 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 main du jeu est visible et qu’elle n’est pas ancrée ou désactivée, le jeu continue de mettre à jour et d’afficher les résultats de cette mise à jour. La méthode GameRenderer::Render que nous examinons ensuite rend 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 celui-ci. Une fois terminé, elle présente la chaîne de permutation finale avec les tampons combinés à afficher.

Notes

Il existe deux états pour la superposition Direct2D de l’exemple de jeu : un dans lequel le jeu affiche la superposition d’informations de jeu qui contient l’image bitmap pour le menu pause, et un autre dans lequel le jeu affiche les points croisés ainsi que les rectangles de la manette de déplacement de l’écran tactile. Le texte des scores est écrit dans les deux états. Pour plus d’informations, consultez Infrastructure de rendu I : introduction au rendu.

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

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 convertisseur pour charger les ressources au niveau de l’appareil. Cette méthode s’exécute dans un thread séparé ; 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 de périphérique sont appelées dans la méthode FinalizeLoadLevel. Si vous débutez dans la programmation asynchrone, consultez Concurrence et opérations asynchrones avec C++/WinRT.
  • FinalizeLoadLevel. Finit tout travail de chargement de niveau à effectuer sur le thread principal. Cela comprend tout appel aux méthodes (ID3D11DeviceContext) de contexte de périphérique Direct3D 11.
  • StartLevel. Démarre le jeu pour un nouveau niveau.
  • PauseGame. Suspend le jeu.
  • RunGame. Exécute une itération de la boucle de jeu. Cette méthode est appelée à partir d’App::Update une fois par itération de la boucle de jeu si le jeu présente l’état Active.
  • 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. Redéfinit l’état de chaque objet sphère utilisé comme munition dans son état d’origine au début de chaque partie.
  • UpdateDynamics. Il s’agit d’une méthode importante, car elle met à jour tous les objets de jeu en fonction des routines d’animation, de la physique et des entrées de contrôle mises en conserve. Elle représente le cœur de l’interactivité qui définit le jeu. Cela 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 gameplay et à la superposition à l’infrastructure de l’application pour l’affichage.

Membres de données

Ces objets sont mis à jour au fur et à mesure que la boucle du 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 convertisseur Direct3D 11, qui gère tous les objets spécifiques à l’appareil et leur rendu. Pour plus d’informations, consultez Infrastructure de rendu I.
  • Objet audio . Contrôle la lecture audio du jeu. Pour plus d’informations, consultez Ajout de son.

Les autres variables de jeu contiennent les listes des primitives et leurs quantités respectives dans le jeu, ainsi que des données et des 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 render sur les primitives mises à jour sont transformés en pixels sur votre écran. Ces aspects sont couverts en deux parties : Infrastructure de rendu I : Introduction au rendu et Framework de rendu II : Rendu du jeu. Si vous êtes plus intéressé par la façon dont les contrôles du joueur mettent à jour l’état du jeu, consultez Ajout de contrôles.