Freigeben über


Definieren Sie das UWP-App-Framework des Spiels

Hinweis

Dieses Thema ist Teil der Tutorialreihe : Erstelle ein einfaches Spiel für die Universelle Windows-Plattform (UWP) mit DirectX. Das Thema an dieser Verknüpfung legt den Kontext für die Datenreihe fest.

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

Ziele

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

Hinweis

Um sich mit diesem Thema zu beschäftigen, schauen Sie sich den Quellcode für das Beispielspiel Simple3DGameDX an, das Sie heruntergeladen haben.

Einleitung

Im Thema Einrichten des Spielprojekts haben wir die Funktion wWinMain 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.

Dieses Thema greift von dort ab und geht ausführlicher darüber ein, wie die App Klasse in einem Spiel die Methoden von IFrameworkViewimplementieren sollte.

Die App::Initialize-Methode

Beim Start der Anwendung ist die erste Methode, die von Windows aufgerufen wird, unsere Implementierung von IFrameworkView::Initialize.

Ihre Implementierung sollte das grundlegendste Verhalten eines UWP-Spiels behandeln, beispielsweise sicherstellen, dass das Spiel ein Suspendierungsereignis (und ein mögliches späteres Wiederaufnahmeereignis) durch das Abonnieren dieser Ereignisse handhaben kann. Wir haben auch Zugriff auf das Displayadaptergerät hier, sodass 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 Rohzeiger wann immer möglich (was fast immer möglich ist).

  • 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 Initializeruft Windows unsere Implementierung von IFrameworkView::SetWindowauf, wobei ein CoreWindow--Objekt übergeben wird, das das Hauptfenster des Spiels darstellt.

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

Wir werden mehr über den Umgang mit Ereignissen im -Thema "Game flow management" sprechen.

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. Ladevorgang ist besser geeignet, 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 er 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 erhebt ein Limit für die Zeit, die Ihr Spiel dauern kann, bevor es mit der Verarbeitung von Eingaben beginnt. Wenn man also asynchron arbeitet, bedeutet das, dass Load schnell zurückgegeben werden kann, während die begonnene Arbeit im Hintergrund weiterläuft. Wenn das Laden sehr lange dauert oder viele Ressourcen vorhanden sind, empfiehlt es sich, Ihren Benutzern eine häufig aktualisierte Statusleiste bereitzustellen.

Wenn Sie mit der asynchronen Programmierung noch nicht 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 Sie ein Objekt vom Typ GameRenderer. Weitere Informationen finden Sie unter Rendering-Framework I: Einführung in das Rendern.
  • Erstellen und initialisieren Sie ein Objekt des Typs Simple3DGame. Weitere Informationen finden Sie unter Define the main game object.
  • 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; der Zugriff auf das Direct3D-Gerät zur Objekterstellung ist jedoch threadunabhängig. Folglich kann die GameRenderer::CreateGameDeviceResourcesAsync-Koroutine auf einem separaten Thread im Vergleich zur Abschlussaufgabe (GameRenderer::FinalizeCreateGameDeviceResources) ausgeführt werden, welche im ursprünglichen Thread läuft.
  • Wir verwenden ein ähnliches Muster zum Laden von Levelressourcen mit Simple3DGame::LoadLevelAsync und Simple3DGame::FinalizeLoadLevel.

Wir werden mehr von GameMain::InitializeGameState im nächsten Thema sehen (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, das Haupt-CoreWindow zu aktivieren. Alternativ können Sie dies auch in App::SetWindowtun.

Die App::Run-Methode

Initialisieren, SetWindowund Load haben die Bühne bereitet. Da das Spiel nun läuft, wird unsere Implementierung von IFrameworkView::Run aufgerufen.

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

Auch hier wird die Arbeit an GameMaindelegiert.

Die GameMain::Run-Methode

GameMain::Run ist die Hauptschleife des Spiels; Sie können es in GameMain.cppfinden. Die grundlegende Logik besteht darin, dass solange das Fenster für Ihr Spiel geöffnet bleibt, alle Ereignisse abwickeln, den Timer aktualisieren und anschließend die Ergebnisse der Grafikpipeline rendern und präsentieren. 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ände im Zustandsautomaten der Spielengine.

  • UpdateEngineState::Deaktiviert. Dadurch wird angegeben, dass das Spielfenster deaktiviert ist (den Fokus verloren hat) oder angedockt ist.
  • UpdateEngineState::TooSmall. Dadurch wird angegeben, dass der Clientbereich zu klein ist, um das Spiel zu rendern.

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

Während Ihr Spielfenster sichtbar ist (Window.Visible ist true), sollten Sie jedes Ereignis in der Nachrichtenwarteschlange behandeln, sobald es eintrifft. Daher sollten Sie CoreWindowDispatch.ProcessEvents mit der Option ProcessAllIfPresent aufrufen. Andere Optionen können Verzögerungen bei der Verarbeitung von Nachrichtenereignissen verursachen, was dazu führen kann, dass Ihr Spiel nicht reagiert oder zu einem Touchverhalten führt, das sich träge anfühlt.

Wenn das Spiel nicht sichtbar ist (Window.Visible ist false), oder wenn es angehalten ist oder wenn es zu klein 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 eintrifft, und verarbeitet dieses Ereignis (sowie alle anderen, die während der Verarbeitung des ersten in die Prozesswarteschlange gelangen). CoreWindowDispatch.ProcessEvents kehrt dann unmittelbar nach der Verarbeitung der Warteschlange zurück.

Im unten stehenden Beispielcode stellt das m_visible-Datenmitglied die Sichtbarkeit des Fensters dar. Wenn das Spiel pausiert ist, ist das Fenster nicht sichtbar. Wenn das Fenster sichtbar ist, bestimmt der Wert m_updateState (ein UpdateEngineState-Enum) andererseits, ob das Fenster deaktiviert ist (fokussiert verloren), zu klein (angekoppelt) 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 Chance, Bereinigungen 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 geschehen muss, wenn das System diesen Speicher zurückgewinnt, einschließlich einer speziellen Bereinigung von Ressourcen, dann legen Sie den Code für diese Bereinigung in Uninitializeab.

In unserem Fall ist App::Uninitialize ein no-op.

void Uninitialize()
{
}

Tipps

Entwickeln Sie beim Erstellen Ihres eigenen Spiels den Startcode gemäß den in diesem Abschnitt beschriebenen Methoden. Hier ist eine einfache Liste mit grundlegenden Vorschlägen für jede Methode.

  • Verwenden Sie Initialisieren, um Ihre Hauptklassen zuzuweisen, und verbinden Sie die grundlegenden Ereignishandler.
  • Verwenden Sie SetWindow-, um fensterspezifische Ereignisse zu abonnieren und um Ihr Hauptfenster an das geräteabhängige Ressourcenobjekt zu übergeben, damit es dieses Fenster beim Erstellen einer Swap-Chain verwenden 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, wie zum Beispiel prozedural generierte Ressourcen, dann tun Sie dies auch hier.

Nächste Schritte

In diesem Thema wurden einige der grundlegenden Struktur eines UWP-Spiels behandelt, das DirectX verwendet. Es empfiehlt sich, diese Methoden zu berücksichtigen, da wir uns in späteren Themen auf einige dieser Methoden beziehen.

Im nächsten Thema – Spielflussverwaltung – befassen wir uns ausführlich mit der Verwaltung von Spielzuständen und der Ereignisbehandlung, um den Spielfluss aufrechtzuerhalten.