Définir l’infrastructure d’application UWP du jeu

Remarque

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

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

Objectifs

  • Configurez l’infrastructure pour un jeu DirectX (UWP) plateforme Windows universelle et implémentez l’ordinateur d’état qui définit le flux de jeu global.

Remarque

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

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 (que vous pouvez voir dans le fichier de code source dans le App.cpp projet Simple3DGameDX) sert de fabrique de fournisseur d’affichage et de fournisseur d’affichage.

Cette rubrique reprend à partir de là et décrit plus en détail la façon dont la classe App dans un jeu doit implémenter les méthodes d’IFrameworkView.

Méthode App::Initialize

Lors du lancement de l’application, la première méthode que Windows appelle est notre implémentation d’IFrameworkView ::Initialize.

Votre implémentation doit gérer les comportements les plus fondamentaux d’un jeu UWP, comme s’assurer que le jeu peut gérer un événement de suspension (et une reprise ultérieure possible) en vous abonnant à ces événements. Nous avons également accès à l’appareil d’adaptateur d’affichage ici. Nous pouvons donc 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 (et c’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 allons voir un exemple de cela 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 d’IFrameworkView::SetWindow, en passant un objet CoreWindow représentant la fenêtre principale du jeu.

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

Nous allons en savoir plus sur la gestion des événements dans la rubrique de gestion des flux de 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

Maintenant que la fenêtre principale est définie, notre implémentation d’IFrameworkView ::Load est appelée. La charge est un meilleur endroit pour pré-extraire les données de jeu ou les ressources que Initialize et SetWindow.

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 faisons 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 des ressources graphiques et initialiser l’ordinateur d’état du jeu. Nous effectuons également toutes les préparations nécessaires avant le début du jeu, telles que la définition d’états de départ ou de valeurs globales.

Windows impose une limite au temps nécessaire à votre jeu avant de commencer à traiter l’entrée. Ainsi, l’utilisation d’async, comme nous le faisons ici, signifie que Load peut retourner rapidement pendant que le travail qu’il a commencé continue en arrière-plan. Si le chargement prend beaucoup de temps ou s’il y a beaucoup de ressources, fournir à vos utilisateurs une barre de progression fréquemment mise à jour est une bonne idée.

Si vous débutez avec la programmation asynchrone, consultez l’accès concurrentiel et les 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 qui est lancée par le constructeur.

  • Créez et initialisez un objet de type GameRenderer. Pour plus d’informations, consultez l’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 de contrôle de l’interface utilisateur du jeu et affichez la superposition des informations de jeu pour afficher une barre de progression au fur et à mesure que les fichiers de ressources se chargent. Pour plus d’informations, consultez Ajout d’une interface utilisateur.
  • Créez un objet contrôleur pour lire l’entrée à partir du contrôleur (contrôleur tactile, souris ou manette sans fil Xbox). Pour plus d’informations, consultez Ajout de contrôles.
  • Définissez deux zones rectangulaires dans les coins inférieur gauche et inférieur droit de l’écran pour les contrôles tactiles de déplacement et de caméra, respectivement. Le joueur utilise le rectangle inférieur gauche (défini dans l’appel à SetMoveRect) comme pavé de contrôle virtuel pour déplacer la caméra vers l’avant et le côté vers l’arrière et le côté vers le côté. Le rectangle inférieur droit (défini par la méthode SetFireRect ) est utilisé comme bouton virtuel pour déclencher l’ammo.
  • Utilisez les coroutines pour décomposer le chargement des ressources en étapes distinctes. L’accès au contexte de l’appareil Direct3D est limité au thread sur lequel le contexte de l’appareil a été créé ; tandis que l’accès à l’appareil Direct3D pour la création d’objets est thread libre. Par conséquent, le 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::FinaliseLoadLevel.

Nous verrons plus de GameMain::InitializeGameState dans la rubrique suivante (gestion des flux de jeu).

Méthode App::OnActivated

Ensuite, l’événement CoreApplicationView::Activated est déclenché. Ainsi, tout gestionnaire d’événements OnActivated dont vous disposez (par exemple, notre méthode App::OnActivated ) est appelé.

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

Le seul travail que nous faisons ici est d’activer le principal CoreWindow. Vous pouvez également choisir de le faire dans App::SetWindow.

Méthode App::Run

Initialiser, SetWindow et Load ont défini la phase. Maintenant que le jeu est opérationnel, notre implémentation d’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 le trouver dans GameMain.cpp. La logique de base est que pendant que la fenêtre de votre jeu reste ouverte, distribuez tous les événements, mettez à jour le minuteur, puis affichez et présentez les résultats du pipeline graphique. En outre, 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 d’état du moteur de jeu.

  • UpdateEngineState::D eactivated. Cela spécifie que la fenêtre de jeu est désactivée (a perdu le focus) ou est alignée.
  • UpdateEngineState::TooSmall. Cela spécifie que la zone cliente est trop petite pour afficher le jeu.

Dans l’un de ces états, le jeu suspend le traitement des événements et attend que la fenêtre soit activée, pour annuler ou redimensionner.

Pendant que votre fenêtre de jeu est visible (Window.Visible est), vous devez gérer chaque événement dans la file d’attente de messages à mesure qu’elle arrive, et vous devez donc appeler CoreWindowDispatch.ProcessEvents avec l’option ProcessAllIfPresent.true D’autres options peuvent entraîner des retards dans le traitement des événements de message, ce qui peut rendre votre jeu sans réponse, ou entraîner des comportements tactiles qui se sentent lentement.

Lorsque le jeu n’est pas visible (Window.Visible est false), ou lorsqu’il est suspendu, ou lorsqu’il est trop petit (il est aligné), vous ne souhaitez pas qu’il consomme des ressources pour distribuer des messages qui ne seront jamais arrivés. Dans ce cas, votre jeu doit utiliser l’option ProcessOneAndAllPending . Cette option bloque jusqu’à ce qu’il obtient un événement, puis traite cet événement (ainsi que les autres qui arrivent dans la file d’attente de processus pendant le traitement du premier). CoreWindowDispatch.ProcessEvents retourne ensuite immédiatement 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. Lorsque le jeu est suspendu, sa fenêtre n’est pas visible. Lorsque la fenêtre est visible, la valeur de m_updateState (énumération UpdateEngineState ) détermine davantage si la fenêtre est désactivée (focus perdu), trop petite (enfichable) ou la 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

Lorsque le jeu se termine, notre implémentation de IFrameworkView::Uninitialize est appelée. C’est notre opportunité d’effectuer propre up. La fermeture de la fenêtre de l’application ne tue pas le processus de l’application ; mais au lieu de cela, il écrit l’état de l’application singleton en mémoire. Si quelque chose de spécial doit se produire lorsque le système récupère cette mémoire, y compris toute propre up spéciale des ressources, placez le code de cette propre up dans Unnitialize.

Dans notre cas, App::Uninitialize est un no-op.

void Uninitialize()
{
}

Conseils

Lors du développement de 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 à tous les événements spécifiques à une fenêtre et passer votre fenêtre principale à votre objet de ressources dépendantes de l’appareil afin qu’elle puisse utiliser cette fenêtre lors de la création d’une chaîne d’échange.
  • Utilisez Load pour gérer toute configuration restante et lancer la création asynchrone d’objets et le chargement des ressources. Si vous devez créer des fichiers ou des données temporaires, tels que des ressources générées de manière procédurale, faites-le également ici.

Étapes suivantes

Cette rubrique a abordé une partie de la structure de base d’un jeu UWP qui utilise DirectX. Il est judicieux de garder ces méthodes à l’esprit, car nous allons nous référer à certains d’entre eux dans des rubriques ultérieures.

Dans la rubrique suivante : Gestion des flux de jeu, nous allons examiner en détail comment gérer les états de jeu et la gestion des événements afin de maintenir le flux du jeu.