Spielablaufverwaltung

Hinweis

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

Das Spiel verfügt jetzt über ein Fenster, hat einige Ereignishandler registriert und Ressourcen asynchron geladen. In diesem Thema wird die Verwendung von Spielzuständen, das Verwalten bestimmter schlüssel spielbezogener Zustände und das Erstellen einer Updateschleife für die Spiel-Engine erläutert. Anschließend lernen wir den Ablauf der Benutzeroberfläche kennen und erfahren schließlich mehr über die Ereignishandler, die für ein UWP-Spiel benötigt werden.

Spielzustände, die zum Verwalten des Spielflusses verwendet werden

Wir nutzen Spielzustände, um den Spielfluss zu verwalten.

Wenn das Simple3DGameDX-Beispielspiel zum ersten Mal auf einem Computer ausgeführt wird, befindet es sich in einem Zustand, in dem kein Spiel gestartet wurde. Wenn das Spiel später ausgeführt wird, kann es sich in einem dieser Zustände befinden.

  • Es wurde kein Spiel gestartet, oder das Spiel befindet sich zwischen den Ebenen (die Hohe Punktzahl ist 0).
  • Die Spielschleife wird ausgeführt und befindet sich in der Mitte einer Ebene.
  • Die Spielschleife wird nicht ausgeführt, weil ein Spiel abgeschlossen wurde (die Highscore hat einen Wert ungleich 0).

Ihr Spiel kann so viele Zustände aufweisen, wie es benötigt. Denken Sie aber daran, dass sie jederzeit beendet werden kann. Und wenn es fortgesetzt wird, erwartet der Benutzer, dass es in dem Zustand fortgesetzt wird, in dem er sich befand, als er beendet wurde.

Verwaltung des Spielzustands

Daher müssen Sie während der Spielinitialisierung das Kaltstarten des Spiels unterstützen und das Spiel nach dem Beenden des Spiels im Flug fortsetzen. Das Simple3DGameDX-Beispiel speichert immer seinen Spielzustand, um den Eindruck zu erwecken, dass es nie aufgehört hat.

Als Reaktion auf ein Angehaltenes Ereignis wird das Gameplay angehalten, aber die Ressourcen des Spiels befinden sich weiterhin im Arbeitsspeicher. Ebenso wird das Resume-Ereignis behandelt, um sicherzustellen, dass das Beispielspiel in dem Zustand fortgesetzt wird, in dem es sich befand, als es angehalten oder beendet wurde. Je nach Zustand hat der Spieler unterschiedliche Möglichkeiten.

  • Wenn das Spiel auf mittlerer Ebene fortgesetzt wird, wird es angehalten, und das Overlay bietet die Option zum Fortfahren.
  • Wenn das Spiel in einem Zustand fortgesetzt wird, in dem das Spiel abgeschlossen ist, werden die Highscores und eine Option zum Spielen eines neuen Spiels angezeigt.
  • Wenn das Spiel schließlich fortgesetzt wird, bevor ein Level gestartet wurde, stellt das Overlay dem Benutzer eine Startoption dar.

Das Beispielspiel unterscheidet nicht, ob das Spiel kalt gestartet, zum ersten Mal ohne Angehalten gestartet oder aus einem angehaltenen Zustand fortgesetzt wird. Dies ist der richtige Entwurf für jede UWP-App.

In diesem Beispiel erfolgt die Initialisierung der Spielzustände in GameMain::InitializeGameState (eine Gliederung dieser Methode wird im nächsten Abschnitt angezeigt).

Hier sehen Sie ein Flussdiagramm, das Sie bei der Visualisierung des Flusses unterstützt. Sie umfasst sowohl die Initialisierung als auch die Updateschleife.

  • Die Initialisierung beginnt auf dem Knoten Start , wenn Sie nach dem aktuellen Spielstatus suchen. Spielcode finden Sie unter GameMain::InitializeGameState im nächsten Abschnitt.

Hauptzustandsautomat für unser Spiel

Die GameMain::InitializeGameState-Methode

Die GameMain::InitializeGameState-Methode wird indirekt über den Konstruktor der GameMain-Klasse aufgerufen, was das Ergebnis einer GameMain-instance in App::Load ist.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

Aktualisieren der Spiel-Engine

Die App::Run-Methode ruft GameMain::Run auf. In GameMain::Run ist ein einfacher Zustandscomputer für die Verarbeitung aller wichtigen Aktionen, die ein Benutzer ausführen kann. Die höchste Ebene dieses Zustandscomputers befasst sich mit dem Laden eines Spiels, dem Spielen einer bestimmten Ebene oder dem Fortsetzen eines Levels, nachdem das Spiel angehalten wurde (vom System oder vom Benutzer).

Im Beispielspiel gibt es drei Hauptzustände (dargestellt durch die UpdateEngineState-Enumeration ), in denen sich das Spiel befinden kann.

  1. UpdateEngineState::WaitingForResources. Die Spielschleife wird durchlaufen, und ein Übergang ist erst möglich, wenn Ressourcen (insbesondere Grafikressourcen) verfügbar sind. Wenn die asynchronen Aufgaben zum Laden von Ressourcen abgeschlossen sind, wird der Status in UpdateEngineState::ResourcesLoaded aktualisiert. Dies geschieht normalerweise zwischen Ebenen, wenn die Ebene neue Ressourcen vom Datenträger, von einem Spielserver oder aus einem Cloud-Back-End lädt. Im Beispielspiel simulieren wir dieses Verhalten, da das Beispiel zu diesem Zeitpunkt keine zusätzlichen Ressourcen pro Ebene benötigt.
  2. UpdateEngineState::WaitingForPress. Die Spielschleife wird durchlaufen, bis eine bestimmte Benutzereingabe erfolgt. Diese Eingabe ist eine Spieleraktion zum Laden eines Spiels, zum Starten einer Ebene oder zum Fortsetzen einer Ebene. Der Beispielcode verweist über die PressResultState-Enumeration auf diese Unterzustände.
  3. UpdateEngineState::D ynamics. Die Spielschleife wird ausgeführt, und der Spieler spielt. Während der Benutzer spielt, überprüft das Spiel drei Bedingungen, unter denen es übergehen kann:
  • GameState::TimeExpired. Ablauf des Zeitlimits für eine Ebene.
  • GameState::LevelComplete. Abschluss eines Levels durch den Spieler.
  • GameState::GameComplete. Abschluss aller Ebenen durch den Spieler.

Ein Spiel ist einfach ein Zustandscomputer, der mehrere kleinere Zustandscomputer enthält. Jeder spezifische Zustand muss durch sehr spezifische Kriterien definiert werden. Übergänge von einem Zustand zu einem anderen müssen auf diskreten Benutzereingaben oder Systemaktionen (z. B. dem Laden von Grafikressourcen) basieren.

Erwägen Sie bei der Planung Ihres Spiels, den gesamten Spielfluss zu zeichnen, um sicherzustellen, dass Sie alle möglichen Aktionen, die der Benutzer oder das System ausführen kann, berücksichtigt haben. Ein Spiel kann sehr kompliziert sein, sodass ein Zustandscomputer ein leistungsfähiges Tool ist, mit dem Sie diese Komplexität visualisieren und verwalten können.

Sehen wir uns den Code für die Updateschleife an.

Die GameMain::Update-Methode

Dies ist die Struktur des Zustandscomputers, der zum Aktualisieren der Spiel-Engine verwendet wird.

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    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)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

Aktualisieren der Benutzeroberfläche

Wir müssen den Spieler über den Zustand des Systems auf dem Laufenden halten und zulassen, dass sich der Spielzustand je nach den Aktionen des Spielers und den Regeln, die das Spiel definieren, ändert. Viele Spiele, einschließlich dieses Beispielspiels, verwenden häufig Benutzeroberflächenelemente, um diese Informationen dem Spieler zu präsentieren. Die Benutzeroberfläche enthält Darstellungen des Spielzustands und anderer spielspezifischer Informationen wie Punktzahl, Munition oder die Anzahl der verbleibenden Chancen. Die Benutzeroberfläche wird auch als Überlagerung bezeichnet, da sie getrennt von der Standard Grafikpipeline gerendert und über der 3D-Projektion platziert wird.

Einige Benutzeroberflächeninformationen werden auch als Head-up-Display (HUD) präsentiert, damit der Benutzer diese Informationen sehen kann, ohne den Standard Gameplay-Bereich vollständig zu entfernen. Im Beispielspiel erstellen wir dieses Overlay mithilfe der Direct2D-APIs. Alternativ können wir dieses Overlay mithilfe von XAML erstellen, das wir unter Erweitern des Beispielspiels erläutern.

Die Benutzeroberfläche verfügt über zwei Komponenten.

  • Das HUD, das die Bewertung und Informationen zum aktuellen Status des Spiels enthält.
  • Die Bitmap für den Pausenmodus – ein schwarzes Rechteck mit Text, das im angehaltenen/unterbrochenen Zustand des Spiels im Vordergrund angezeigt wird. Dies ist die Spielüberlagerung. Ausführlichere Informationen hierzu finden Sie unter Hinzufügen einer Benutzeroberfläche.

Wie nicht anders zu erwarten, besitzt auch das Overlay einen Zustandsautomaten. Die Überlagerung kann einen Levelstart oder eine Game-Over-Nachricht anzeigen. Es handelt sich im Wesentlichen um eine Canvas, auf der wir alle Informationen zum Spielzustand ausgeben können, die wir dem Spieler anzeigen möchten, während das Spiel angehalten oder angehalten wird.

Das gerenderte Overlay kann je nach Spielstatus einer dieser sechs Bildschirme sein.

  1. Statusbildschirm zum Laden von Ressourcen zu Beginn des Spiels.
  2. Bildschirm "Spielstatistik".
  3. Bildschirm "Startnachricht auf Ebene".
  4. Game-over-Bildschirm, wenn alle Ebenen abgeschlossen sind, ohne dass die Zeit abgelaufen ist.
  5. Game-over-Bildschirm, wenn die Zeit abgelaufen ist.
  6. Menübildschirm anhalten.

Wenn Sie Ihre Benutzeroberfläche von der Grafikpipeline Ihres Spiels trennen, können Sie unabhängig von der Grafikrendering-Engine des Spiels daran arbeiten und die Komplexität des Spielcodes erheblich verringern.

Hier erfahren Sie, wie das Beispielspiel den Zustandscomputer des Overlays strukturiert.

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

Ereignisbehandlung

Wie wir im Thema Definieren des UWP-App-Frameworks des Spiels gesehen haben, registrieren viele der Ansichtsanbietermethoden der App-Klasse Ereignishandler. Diese Methoden müssen diese wichtigen Ereignisse ordnungsgemäß behandeln, bevor wir Spielmechaniken hinzufügen oder mit der Grafikentwicklung beginnen.

Die ordnungsgemäße Behandlung der betreffenden Ereignisse ist für die UWP-App von grundlegender Bedeutung. Da eine UWP-App jederzeit aktiviert, deaktiviert, größesänderung, angedockt, unsnapped, angehalten oder fortgesetzt werden kann, muss sich das Spiel so schnell wie möglich für diese Ereignisse registrieren und sie so behandeln, dass die Erfahrung für den Spieler reibungslos und vorhersagbar bleibt.

Dies sind die in diesem Beispiel verwendeten Ereignishandler und die von ihnen behandelten Ereignisse.

Ereignishandler BESCHREIBUNG
OnActivated Verarbeitet CoreApplicationView::Activated. Die Spiele-App befindet sich im Vordergrund, weshalb das Hauptfenster aktiviert ist.
OnDpiChanged Behandelt Graphics::D isplay::D isplayInformation::D piChanged. Der DPI-Wert der Anzeige hat sich geändert, und das Spiel passt seine Ressourcen entsprechend an.
HinweisCoreWindow-Koordinaten befinden sich in geräteunabhängigen Pixeln (DEVICE-Independent Pixels, DIPs) für Direct2D. Daher müssen Sie Direct2D über die DPI-Änderung informieren, damit die 2D-Ressourcen oder -Grundtypen korrekt angezeigt werden.
OnOrientationChanged Behandelt Graphics::D isplay::D isplayInformation::OrientationChanged. Die Ausrichtung der Anzeigeänderungen und das Rendering müssen aktualisiert werden.
OnDisplayContentsInvalidated Behandelt Graphics::D isplay::D isplayInformation::D isplayContentsInvalidated. Die Anzeige erfordert eine Neuzeichnung, und Ihr Spiel muss erneut gerendert werden.
OnResuming Behandelt CoreApplication::Resuming. Die Spiele-App stellt das Spiel aus einem Anhaltezustand wieder her.
OnSuspending Verarbeitet CoreApplication::Suspending. Die Spiele-App speichert den eigenen Zustand auf einem Datenträger. Der Speichervorgang für den Zustand darf maximal fünf Sekunden dauern.
OnVisibilityChanged Behandelt CoreWindow::VisibilityChanged. Die Sichtbarkeit der Spiele-App hat sich geändert: Die App wurde entweder sichtbar, oder sie wurde durch eine andere sichtbar gewordene App unsichtbar.
OnWindowActivationChanged Behandelt CoreWindow::Activated. Das Hauptfenster der Spiele-App wurde deaktiviert oder aktiviert, weshalb der Fokus entfernt und das Spiel angehalten oder der Fokus wiedererlangt werden muss. In beiden Fällen gibt das Overlay an, dass das Spiel angehalten wurde.
OnWindowClosed Behandelt CoreWindow::Closed. Die Spiele-App schließt das Hauptfenster und hält das Spiel an.
OnWindowSizeChanged Verarbeitet CoreWindow::SizeChanged. Die Spiele-App ordnet die Grafikressourcen und das Overlay neu zu, um die Größenänderung umzusetzen, und aktualisiert anschließend das Renderziel.

Nächste Schritte

In diesem Thema haben wir erfahren, wie der gesamte Spielfluss mithilfe von Spielzuständen verwaltet wird und dass ein Spiel aus mehreren verschiedenen Zustandscomputern besteht. Wir haben auch erfahren, wie Sie die Benutzeroberfläche aktualisieren und wichtige App-Ereignishandler verwalten. Jetzt sind wir bereit, uns mit der Renderingschleife, dem Spiel und seinen Mechaniken vertraut zu machen.

Sie können die restlichen Themen, die dieses Spiel dokumentieren, in beliebiger Reihenfolge durchgehen.