Partager via


Définir l’infrastructure d’application UWP du 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.

La première étape du codage d’un jeu sur la plateforme Windows universelle (UWP) consiste à créer l’infrastructure qui permet à l’objet application d’interagir avec Windows, notamment les fonctionnalités de Windows Runtime telles que la gestion des événements d’interruption et de reprise, les modifications de la visibilité des fenêtres et l’alignement.

Objectifs

  • Configurer l’infrastructure pour un jeu DirectX sur la plateforme Windows universelle (UWP) et implémenter la machine à états qui définit le flux global du jeu.

Remarque

Pour suivre cette rubrique, recherchez dans le code source que vous avez téléchargé l’exemple de jeu Simple3DGameDX.

Présentation

Dans la rubrique Configurer le projet de jeu, nous avons introduit la fonction wWinMain ainsi que les interfaces IFrameworkViewSource et IFrameworkView. Nous avons appris que la classe App (qui est définie dans le fichier de code source App.cpp du projet Simple3DGameDX) sert à la fois de fabrique view-provider et de view-provider.

C’est le point de départ de cette rubrique qui explique beaucoup plus en détail comment la classe App d’un jeu doit implémenter les méthodes de IFrameworkView.

Méthode App::Initialize

Au lancement de l’application, la première méthode appelée par Windows est notre implémentation de IFrameworkView::Initialize.

Votre implémentation doit gérer les comportements les plus fondamentaux d’un jeu UWP, notamment en s’assurant que le jeu peut gérer un événement d’interruption (et une éventuelle reprise ultérieure) en s’abonnant à ces événements. Nous avons également accès à la carte vidéo ici, ce qui nous permet de créer des ressources graphiques qui dépendent de l’appareil.

void Initialize(CoreApplicationView const& applicationView)
{
    applicationView.Activated({ this, &App::OnActivated });

    CoreApplication::Suspending({ this, &App::OnSuspending });

    CoreApplication::Resuming({ this, &App::OnResuming });

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

Évitez les pointeurs bruts dans la mesure du possible (ce qui est presque toujours possible).

  • Pour les types Windows Runtime, vous pouvez très souvent éviter les pointeurs et construire simplement une valeur sur la pile. Si vous avez besoin d’un pointeur, utilisez winrt::com_ptr (nous en verrons un exemple bientôt).
  • Pour les pointeurs uniques, utilisez std::unique_ptr et std::make_unique.
  • Pour les pointeurs partagés, utilisez std::shared_ptr et std::make_shared.

Méthode App::SetWindow

Après Initialize, Windows appelle notre implémentation de IFrameworkView::SetWindow en passant un objet CoreWindow représentant la fenêtre principale du jeu.

Dans App::SetWindow, nous nous abonnons aux événements liés aux fenêtres et configurons certains comportements relatifs aux fenêtres et à l’affichage. Par exemple, nous construisons un pointeur de souris, via la classe CoreCursor, qui peut être utilisé à la fois par les contrôles souris et tactiles. Nous passons également l’objet window à notre objet de ressources dépendantes de l’appareil.

Nous aborderons plus en détail la gestion des événements dans la rubrique Gestion du flux du jeu.

void SetWindow(CoreWindow const& window)
{
    //CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();

    window.PointerCursor(CoreCursor(CoreCursorType::Arrow, 0));

    PointerVisualizationSettings visualizationSettings{ PointerVisualizationSettings::GetForCurrentView() };
    visualizationSettings.IsContactFeedbackEnabled(false);
    visualizationSettings.IsBarrelButtonFeedbackEnabled(false);

    m_deviceResources->SetWindow(window);

    window.Activated({ this, &App::OnWindowActivationChanged });

    window.SizeChanged({ this, &App::OnWindowSizeChanged });

    window.Closed({ this, &App::OnWindowClosed });

    window.VisibilityChanged({ this, &App::OnVisibilityChanged });

    DisplayInformation currentDisplayInformation{ DisplayInformation::GetForCurrentView() };

    currentDisplayInformation.DpiChanged({ this, &App::OnDpiChanged });

    currentDisplayInformation.OrientationChanged({ this, &App::OnOrientationChanged });

    currentDisplayInformation.StereoEnabledChanged({ this, &App::OnStereoEnabledChanged });

    DisplayInformation::DisplayContentsInvalidated({ this, &App::OnDisplayContentsInvalidated });
}

Méthode App::Load

Une fois la fenêtre principale définie, notre implémentation de IFrameworkView::Load est appelée. Load est un meilleur emplacement que Initialize et SetWindow pour préextraire des données ou des ressources de jeu.

void Load(winrt::hstring const& /* entryPoint */)
{
    if (!m_main)
    {
        m_main = winrt::make_self<GameMain>(m_deviceResources);
    }
}

Comme vous pouvez le voir, le travail réel est délégué au constructeur de l’objet GameMain que nous créons ici. La classe GameMain est définie dans GameMain.h et GameMain.cpp.

Constructeur GameMain::GameMain

Le constructeur GameMain (et les autres fonctions membres qu’il appelle) commence un ensemble d’opérations de chargement asynchrones pour créer les objets de jeu, charger les ressources graphiques et initialiser la machine à états du jeu. Nous effectuons également toutes les préparations nécessaires avant le début du jeu, comme la définition des états de départ ou des valeurs globales.

Windows impose une limite au temps que votre jeu peut prendre avant de commencer à traiter les entrées. Ainsi, en utilisant async, comme nous le faisons ici, Load peut être rapidement de retour pendant que le travail qu’il a commencé continue en arrière-plan. Si le chargement prend beaucoup de temps ou s’il existe de nombreuses ressources, il est judicieux de fournir à vos utilisateurs une barre de progression fréquemment mise à jour.

Si vous débutez avec la programmation asynchrone, consultez Concurrence et opérations asynchrones avec C++/WinRT.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) :
    m_deviceResources(deviceResources),
    m_windowClosed(false),
    m_haveFocus(false),
    m_gameInfoOverlayCommand(GameInfoOverlayCommand::None),
    m_visible(true),
    m_loadingCount(0),
    m_updateState(UpdateEngineState::WaitingForResources)
{
    m_deviceResources->RegisterDeviceNotify(this);

    m_renderer = std::make_shared<GameRenderer>(m_deviceResources);
    m_game = std::make_shared<Simple3DGame>();

    m_uiControl = m_renderer->GameUIControl();

    m_controller = std::make_shared<MoveLookController>(CoreWindow::GetForCurrentThread());

    auto bounds = m_deviceResources->GetLogicalSize();

    m_controller->SetMoveRect(
        XMFLOAT2(0.0f, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(GameUIConstants::TouchRectangleSize, bounds.Height)
        );
    m_controller->SetFireRect(
        XMFLOAT2(bounds.Width - GameUIConstants::TouchRectangleSize, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(bounds.Width, bounds.Height)
        );

    SetGameInfoOverlay(GameInfoOverlayState::Loading);
    m_uiControl->SetAction(GameInfoOverlayCommand::None);
    m_uiControl->ShowGameInfoOverlay();

    // Asynchronously initialize the game class and load the renderer device resources.
    // By doing all this asynchronously, the game gets to its main loop more quickly
    // and in parallel all the necessary resources are loaded on other threads.
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    auto lifetime = get_strong();

    m_game->Initialize(m_controller, m_renderer);

    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);

    // The finalize code needs to run in the same thread context
    // as the m_renderer object was created because the D3D device context
    // can ONLY be accessed on a single thread.
    // co_await of an IAsyncAction resumes in the same thread context.
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();

    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        // In the middle of a game so spin up the async task to load the level.
        co_await m_game->LoadLevelAsync();

        // The m_game object may need to deal with D3D device context work so
        // again the finalize code needs to run in the same thread
        // context as the m_renderer object was created because the D3D
        // device context can ONLY be accessed on a single thread.
        m_game->FinalizeLoadLevel();
        m_game->SetCurrentLevelToSavedState();
        m_updateState = UpdateEngineState::ResourcesLoaded;
    }
    else
    {
        // The game is not in the middle of a level so there aren't any level
        // resources to load.
    }

    // Since Game loading is an async task, the app visual state
    // may be too small or not be activated. Put the state machine
    // into the correct state to reflect these cases.

    if (m_deviceResources->GetLogicalSize().Width < GameUIConstants::MinPlayableWidth)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::TooSmall;
        m_controller->Active(false);
        m_uiControl->HideGameInfoOverlay();
        m_uiControl->ShowTooSmall();
        m_renderNeeded = true;
    }
    else if (!m_haveFocus)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::Deactivated;
        m_controller->Active(false);
        m_uiControl->SetAction(GameInfoOverlayCommand::None);
        m_renderNeeded = true;
    }
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    ...
}

Voici un plan de la séquence de travail lancée par le constructeur.

  • Créez et initialisez un objet de type GameRenderer. Pour plus d’informations, consultez Infrastructure de rendu I : présentation du rendu.
  • Créez et initialisez un objet de type Simple3DGame. Pour plus d’informations, consultez Définir l’objet de jeu principal.
  • Créez l’objet uicontrol et affichez la superposition d’informations du jeu pour présenter une barre de progression du chargement des fichiers de ressources. Pour plus d’informations, consultez Ajout d’une interface utilisateur.
  • Créez un objet controller pour lire l’entrée à partir du contrôleur (tactile, souris ou contrôleur de jeu). Pour plus d’informations, consultez Ajout de contrôles.
  • Définissez deux zones rectangulaires dans les coins inférieurs gauche et droit de l’écran pour les contrôles tactiles de déplacement et de la caméra, respectivement. Le joueur se sert du rectangle inférieur gauche, défini dans l’appel à SetMoveRect, comme d’un boîtier de commande virtuel pour déplacer la caméra vers l’avant et vers l’arrière, et d’un côté à l’autre. Le rectangle inférieur droit, défini par la méthode SetFireRect, sert de bouton virtuel pour tirer des munitions.
  • Utilisez des coroutines pour diviser le chargement des ressources en étapes distinctes. L’accès au contexte de périphérique Direct3D est limité au thread sur lequel le contexte de périphérique a été créé, tandis que l’accès au périphérique Direct3D pour la création d’objets est à threads libres. Par conséquent, la coroutine GameRenderer::CreateGameDeviceResourcesAsync peut s’exécuter sur un thread distinct de la tâche d’achèvement (GameRenderer::FinaliseCreateGameDeviceResources), qui s’exécute sur le thread d’origine.
  • Nous utilisons un modèle similaire pour charger des ressources de niveau avec Simple3DGame::LoadLevelAsync et Simple3DGame::FinalizeLoadLevel.

Nous examinerons plus en détail GameMain::InitializeGameState dans la rubrique suivante (Gestion du flux du jeu).

Méthode App::OnActivated

Ensuite, l’événement CoreApplicationView::Activated est déclenché. Tout gestionnaire d’événements OnActivated dont vous disposez (comme notre méthode App::OnActivated) est donc appelé.

void OnActivated(CoreApplicationView const& /* applicationView */, IActivatedEventArgs const& /* args */)
{
    CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();
}

Le seul travail que nous effectuons ici consiste à activer le CoreWindow principal. Vous pouvez également effectuer cette opération dans App::SetWindow.

Méthode App::Run

Initialize, SetWindow et Load ont préparé le terrain. Maintenant que le jeu est opérationnel, notre implémentation de IFrameworkView::Run est appelée.

void Run()
{
    m_main->Run();
}

Là encore, le travail est délégué à GameMain.

Méthode GameMain::Run

GameMain::Run est la boucle principale du jeu ; vous pouvez la trouver dans GameMain.cpp. La logique de base est la suivante : tant que la fenêtre de votre jeu est ouverte, distribuer tous les événements, mettre à jour le minuteur, puis afficher et présenter les résultats du pipeline graphique. Ici aussi, les événements utilisés pour passer d’un état de jeu à l’autre sont distribués et traités.

Le code ici concerne également deux des états de la machine à états du moteur de jeu.

  • UpdateEngineState::Deactivated. Spécifie que la fenêtre de jeu est désactivée (a perdu le focus) ou est alignée.
  • UpdateEngineState::TooSmall. Spécifie que la zone du client est trop petite pour afficher le jeu.

Dans l’un ou l’autre de ces états, le jeu interrompt le traitement des événements et attend que la fenêtre soit activée ou redimensionnée ou qu’elle ne soit plus alignée.

Lorsque votre fenêtre de jeu est visible (Window.Visible est true), vous devez gérer chaque événement qui arrive dans la file d’attente de messages. Vous devez donc appeler CoreWindowDispatch.ProcessEvents avec l’option ProcessAllIfPresent. D’autres options peuvent provoquer des retards dans le traitement des événements de message, ce qui peut donner la sensation que votre jeu ne répond pas ou que les comportements tactiles sont au ralenti.

Lorsque le jeu n’est pas visible (Window.Visible est false), qu’il est interrompu ou qu’il est trop petit (il est aligné), vous ne voulez pas qu’il consomme des ressources qui, au cours de leur cycle, distribueront des messages qui n’arriveront jamais. Dans ce cas, votre jeu doit utiliser l’option ProcessOneAndAllPending. Cette option bloque le jeu jusqu’à l’obtention d’un événement, puis traite cet événement (ainsi que les autres qui arrivent dans la file d’attente du processus pendant le traitement du premier). CoreWindowDispatch.ProcessEvents est ensuite immédiatement de retour une fois la file d’attente traitée.

Dans l’exemple de code ci-dessous, le membre de données m_visible représente la visibilité de la fenêtre. Quand le jeu est interrompu, sa fenêtre n’est pas visible. Quand la fenêtre est visible, la valeur de m_updateState (une énumération UpdateEngineState) détermine si la fenêtre est désactivée (focus perdu), si elle est trop petite (alignée) ou si elle est de taille appropriée.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                if (m_updateStateNext == UpdateEngineState::WaitingForResources)
                {
                    WaitingForResourceLoading();
                    m_renderNeeded = true;
                }
                else if (m_updateStateNext == UpdateEngineState::ResourcesLoaded)
                {
                    // In the device lost case, we transition to the final waiting state
                    // and make sure the display is updated.
                    switch (m_pressResult)
                    {
                    case PressResultState::LoadGame:
                        SetGameInfoOverlay(GameInfoOverlayState::GameStats);
                        break;

                    case PressResultState::PlayLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::LevelStart);
                        break;

                    case PressResultState::ContinueLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::Pause);
                        break;
                    }
                    m_updateStateNext = UpdateEngineState::WaitingForPress;
                    m_uiControl->ShowGameInfoOverlay();
                    m_renderNeeded = true;
                }

                if (!m_renderNeeded)
                {
                    // The App is not currently the active window and not in a transient state so just wait for events.
                    CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
                    break;
                }
                // otherwise fall through and do normal processing to get the rendering handled.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                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.
}

Méthode App::Uninitialize

À la fin du jeu, notre implémentation de IFrameworkView::Uninitialize est appelée. C’est l’occasion d’effectuer un nettoyage. La fermeture de la fenêtre de l’application ne met pas fin au processus de l’application, mais écrit l’état du singleton de l’application en mémoire. Si quelque chose de spécial doit se produire lorsque le système récupère cette mémoire, notamment un nettoyage spécial des ressources, placez le code de ce nettoyage dans Uninitialize.

Dans notre cas, App::Uninitialize est une instruction nulle (« no-op »).

void Uninitialize()
{
}

Conseils

Quand vous développez votre propre jeu, concevez votre code de démarrage autour des méthodes décrites dans cette rubrique. Voici une liste simple de suggestions de base pour chaque méthode.

  • Utilisez Initialize pour allouer vos classes principales et connecter les gestionnaires d’événements de base.
  • Utilisez SetWindow pour vous abonner à des événements propres aux fenêtres et passer votre fenêtre principale à votre objet de ressources dépendantes de l’appareil afin qu’il puisse utiliser cette fenêtre lors de la création d’une chaîne d’échange.
  • Utilisez Load pour gérer le reste de l’installation, notamment pour lancer la création asynchrone d’objets et le chargement des ressources. Si vous devez créer des données ou des fichiers temporaires, par exemple des composants générés par procédure, faites-le également ici.

Étapes suivantes

Cette rubrique a couvert une partie de la structure de base d’un jeu UWP qui utilise DirectX. Il est recommandé de garder ces méthodes à l’esprit, car nous reviendrons sur certaines d’entre elles lors de rubriques ultérieures.

Dans la rubrique suivante, Gestion du flux du jeu, nous allons examiner en détail comment gérer les états et les événements du jeu pour assurer le bon déroulement du jeu.