Definieren des UWP-App-Frameworks für das Spiel

Hinweis

Dieses Thema ist Teil der Erstellen eines einfachen UWP-Spiels (Universelle Windows-Plattform) mit DirectX -Tutorial-Reihe. Das Thema unter diesem Link legt den Kontext für die Reihe fest.

Der erste Schritt beim Programmieren eines UWP-Spiels (Universelle Windows-Plattform-Spiels) besteht darin, das Framework zu erstellen, mit dem das App-Objekt mit Windows interagieren kann, einschließlich Windows-Runtime-Features wie die Behandlung von Ereignissen zum Anhalten und Fortsetzen, Änderungen der Fenstersichtbarkeit und Andocken.

Ziele

  • Richten Sie das Framework für ein UWP-DirectX-Spiel (Universelle Windows-Plattform) ein, und implementieren Sie die Zustandsmaschine, der den allgemeinen Spielfluss definiert.

Hinweis

Um diesem Thema zu folgen, sehen Sie sich den Quellcode für das Simple3DGameDX -Testspiel an, das Sie heruntergeladen haben.

Einführung

Im Thema Einrichten des Spielprojekts wurde die wWinMain -Funktion sowie die Schnittstellen IFrameworkViewSource und IFrameworkView vorgestellt. Wir haben gelernt, dass die App -Klasse (die Sie in der App.cpp Quellcodedatei im Simple3DGameDX -Projekt definiert sehen können) sowohl als Ansichtsanbieter-Factory als auch als Ansichtsanbieter dient.

In diesem Thema wird von dort aufgenommen und ausführlicher erläutert, wie die App -Klasse in einem Spiel die Methoden von IFrameworkViewimplementieren sollte.

Die App::Initialize-Methode

Beim Starten der Anwendung ist die erste Methode, die Windows aufruft, die Implementierung von IFrameworkView::Initialize.

Ihre Implementierung sollte die grundlegendsten Verhaltensweisen eines UWP-Spiels behandeln, z. B. sicherstellen, dass das Spiel einen Anhalte-Vorgang (und ein mögliches späteres Fortsetzen) verarbeiten kann, indem diese Ereignisse abonniert werden. Wir haben auch Zugriff auf das Grafikkartengerät hier, damit wir Grafikressourcen erstellen können, die vom Gerät abhängig sind.

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

Vermeiden Sie nach Möglichkeit unformatierte Zeiger (und es ist fast immer möglich).

  • Bei Windows-Runtime-Typen können Sie Zeiger häufig ganz vermeiden und nur einen Wert auf dem Stapel erstellen. Wenn Sie einen Zeiger benötigen, verwenden Sie winrt::com_ptr (es wird bald ein Beispiel dafür angezeigt).
  • Verwenden Sie für eindeutige Zeiger std::unique_ptr und std::make_unique.
  • Verwenden Sie für freigegebene Zeiger std::shared_ptr und std::make_shared.

Die App::SetWindow-Methode

Nach der Initialisierung ruft Windows unsere Implementierung von IFrameworkView::SetWindow auf und übergibt ein CoreWindow -Objekt, das das Standard Fenster des Spiels darstellt.

In App::SetWindow abonnieren wir fensterbezogene Ereignisse und konfigurieren einige Fenster- und Anzeigeverhalten. Beispielsweise erstellen wir einen Mauszeiger (über die CoreCursor -Klasse ), der sowohl von Maus- als auch von Touchsteuerelementen verwendet werden kann. Außerdem übergeben wir das Fensterobjekt an unser geräteabhängiges Ressourcenobjekt.

Weitere Informationen zum Behandeln von Ereignissen finden Sie im Thema Game Flow Management.

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

Die App::Load-Methode

Nachdem das Hauptfenster festgelegt ist, wird unsere Implementierung von IFrameworkView::Load aufgerufen. Load ist ein besserer Ort, um Spieldaten oder Ressourcen vorab abzurufen, als Initialize und SetWindow.

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

Wie Sie sehen können, wird die eigentliche Arbeit an den Konstruktor des GameMain -Objekts delegiert, das wir hier erstellen. Die GameMain -Klasse wird in GameMain.h und GameMain.cppdefiniert.

Der GameMain::GameMain-Konstruktor

Der GameMain -Konstruktor (und die anderen Memberfunktionen, die es aufruft) beginnt eine Reihe asynchroner Ladevorgänge, um die Spielobjekte zu erstellen, Grafikressourcen zu laden und den Zustandsautomaten des Spiels zu initialisieren. Wir treffen auch alle erforderlichen Vorbereitungen, bevor das Spiel beginnt, z. B. das Festlegen von Startzuständen oder globalen Werten.

Windows erzwingt ein Limit für die Zeit, die Ihr Spiel dauern kann, bevor die Verarbeitung der Eingabe beginnt. Die Verwendung von asynchronen Methoden bedeutet also, dass Load schnell zurückkehren kann, während die Arbeit, mit dem es begonnen hat, im Hintergrund fortgesetzt wird. Wenn das Laden sehr lange dauert oder viele Ressourcen vorhanden sind, empfiehlt es sich, Ihren Benutzern eine häufig aktualisierte Statusleiste bereitzustellen.

Wenn Sie noch nicht mit der asynchronen Programmierung vertraut sind, lesen Sie Parallelität und asynchrone Vorgänge mit 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.
    ...
}

Hier ist eine Gliederung der Arbeitssequenz, die vom Konstruktor gestartet wird.

  • Erstellen und Initialisieren eines Objekts vom Typ GameRenderer. Weitere Informationen finden Sie unter Renderingframework I: Einführung in Rendering.
  • Erstellen und Initialisieren eines Objekts vom Typ Simple3DGame. Weitere Informationen finden Sie unter Definieren des Hauptspielobjekts.
  • Erstellen Sie das Spiel-UI-Steuerelementobjekt, und zeigen Sie die Spielinfoüberlagerung an, um eine Statusanzeige anzuzeigen, während die Ressourcendateien geladen werden. Weitere Informationen finden Sie unter Hinzufügen einer Benutzeroberfläche.
  • Erstellen Sie ein Controllerobjekt zum Lesen von Eingaben vom Controller (Toucheingabe, Maus oder Gamecontroller). Weitere Informationen finden Sie unter Hinzufügen von Steuerelementen.
  • Definieren Sie zwei rechteckige Bereiche in der unteren linken und unteren rechten Ecke des Bildschirms für die Bewegungs- bzw. Kameraeingabesteuerungen. Der Spieler verwendet das untere linke Rechteck (definiert im Aufruf von SetMoveRect) als virtuelles Steuerkreuz, um die Kamera vorwärts und rückwärts und seitlich zu bewegen. Das untere rechte Rechteck (definiert durch die SetFireRect -Methode) wird als virtuelle Schaltfläche verwendet, um die Munition auszulösen.
  • Verwenden Sie Coroutinen, um das Laden von Ressourcen in separate Phasen aufzubrechen. Der Zugriff auf den Direct3D-Gerätekontext ist auf den Thread beschränkt, auf dem der Gerätekontext erstellt wurde; während der Zugriff auf das Direct3D-Gerät für die Objekterstellung Thread-unabhängig ist. Folglich kann die GameRenderer::CreateGameDeviceResourcesAsync -Coroutine auf einem separaten Thread von der Fertigstellungsaufgabe (GameRenderer::FinalizeCreateGameDeviceResources) ausgeführt werden, die im ursprünglichen Thread ausgeführt wird.
  • Wir verwenden ein ähnliches Muster zum Laden von Ressourcen mit Simple3DGame::LoadLevelAsync und Simple3DGame::FinalizeLoadLevel.

Weitere Informationen zu GameMain::InitializeGameState finden Sie im nächsten Thema (Spielflussverwaltung).

Die App::OnActivated-Methode

Als Nächstes wird das CoreApplicationView::Activated -Ereignis ausgelöst. Daher wird jeder OnActivated -Ereignishandler aufgerufen, den Sie haben (z. B. unsere App::OnActivated -Methode).

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

Die einzige Arbeit, die wir hier ausführen, besteht darin, Haupt- CoreWindow zu aktivieren. Alternativ können Sie dies in App::SetWindow ausführen.

Die App::Run-Methode

Initialisieren, SetWindow-und Load haben die Voraussetzungen geschafft. Nachdem das Spiel nun ausgeführt wird, wird unsere Implementierung von IFrameworkView::Run aufgerufen.

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

Auch hier wird die Arbeit an GameMain delegiert.

Die GameMain::Run-Methode

GameMain::Run ist die Hauptschleife des Spiels; sie finden Sie in GameMain.cpp. Die grundlegende Logik besteht darin, dass während das Fenster für Ihr Spiel geöffnet bleibt, alle Ereignisse verteilt werden, der Timer aktualisiert wird und dann die Ergebnisse der Grafikpipeline gerendert und dargestellt werden. Auch hier werden die Ereignisse, die für den Übergang zwischen Spielzuständen verwendet werden, verteilt und verarbeitet.

Der Code befasst sich hier auch mit zwei der Zuständen im Zustandsautomaten des Spielmotors.

  • UpdateEngineState::Deactivated. Dies gibt an, dass das Spielfenster deaktiviert ist (den Fokus verloren hat) oder angedockt ist.
  • UpdateEngineState::TooSmall. Dies gibt an, dass der Clientbereich zu klein ist, um das Spiel darin zu rendern.

In einem dieser Zustände hält das Spiel die Ereignisverarbeitung an und wartet darauf, dass das Fenster aktiviert, aufgehoben oder die Größe geändert wird.

Während ihr Spielfenster sichtbar ist (Window.Visible ist true), müssen Sie jedes Ereignis in der Nachrichtenwarteschlange verarbeiten, während es eingeht. Daher müssen Sie CoreWindowDispatch.ProcessEvents mit der Option ProcessAllIfPresent aufrufen. Andere Optionen können zu Verzögerungen bei der Verarbeitung von Nachrichtenereignissen führen, was dazu führen kann, dass ihr Spiel nicht reagiert, oder zu verzögerten Touchverhalten führt.

Wenn das Spiel nicht sichtbar ist (Window.Visible ist false), oder wenn es angehalten ist oder wenn es zu klein ist (es ist angedockt), soll es keine Ressourcen verbrauchen, um Nachrichten zu senden, die nie eingehen. In diesem Fall muss Ihr Spiel die Option ProcessOneAndAllPending verwenden. Diese Option blockiert, bis ein Ereignis abgerufen wird, und verarbeitet dieses Ereignis (sowie alle anderen, die während der Verarbeitung der ersten In die Prozesswarteschlange gelangen). CoreWindowDispatch.ProcessEvents kehrt dann unmittelbar nach der Verarbeitung der Warteschlange zurück.

Im unten gezeigten Beispielcode stellt der m_visible -Datenmember die Sichtbarkeit des Fensters dar. Wenn das Spiel angehalten wird, ist das Fenster nicht sichtbar. Wenn das Fenster sichtbar ist, bestimmt der Wert m_updateState (ein UpdateEngineState -Enumeration) weiter, ob das Fenster deaktiviert ist (verlorener Fokus), zu klein ist (angedockt) oder die richtige Größe hat.

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

Die App::Uninitialize-Methode

Wenn das Spiel endet, wird unsere Implementierung von IFrameworkView::Uninitialize aufgerufen. Dies ist unsere Gelegenheit, eine Bereinigung durchzuführen. Durch das Schließen des App-Fensters wird der Prozess der App nicht beendet. stattdessen wird der Zustand des App-Singletons in den Arbeitsspeicher geschrieben. Wenn etwas Besonderes passieren muss, wenn das System diesen Speicher zurückgibt, einschließlich einer speziellen Bereinigung von Ressourcen, legen Sie den Code für diese Bereinigung in Uninitialize.

In unserem Fall ist App::Uninitialize eine Nulloperation.

void Uninitialize()
{
}

Tipps

Entwerfen Sie beim Entwickeln Ihres eigenen Spiels Ihren Startcode anhand der in diesem Thema beschriebenen Methoden. Es folgt eine Liste mit einfachen Vorschlägen für die einzelnen Methoden.

  • Verwenden Sie Initialize, um Ihre Hauptklassen zuzuordnen und die grundlegenden Ereignishandler zu verbinden.
  • Verwenden Sie SetWindow, um fensterspezifische Ereignisse zu abonnieren und Ihr Standardfenster an Ihr geräteabhängiges Ressourcenobjekt zu übergeben, damit dieses Fenster beim Erstellen einer Swapchain verwendet werden kann.
  • Verwenden Sie Load, um alle verbleibenden Setups zu verarbeiten und die asynchrone Erstellung von Objekten zu initiieren und Ressourcen zu laden. Wenn Sie temporäre Dateien oder Daten erstellen müssen, z. B. prozedurell generierte Ressourcen, führen Sie dies auch hier aus.

Nächste Schritte

In diesem Thema wurden einige der grundlegenden Strukturen eines UWP-Spiels behandelt, das DirectX verwendet. Es ist eine gute Idee, diese Methoden im Auge zu behalten, da wir in späteren Themen auf einige davon zurückgreifen werden.

Im nächsten Thema – Spielflussverwaltung – werden wir uns eingehend mit der Verwaltung von Spielzuständen und der Ereignisbehandlung befassen, um das Spiel am Laufen zu halten.