Freigeben über


Spielablaufverwaltung

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.

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 Spielzustände und das Erstellen einer Updateschleife für die Spielengine erläutert. Anschließend erfahren wir mehr über den Benutzeroberflächenfluss und erfahren schließlich mehr über die Ereignishandler, die für ein UWP-Spiel erforderlich sind.

Spielzustände, die zum Verwalten des Spielflusses verwendet werden

Wir verwenden 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. In den nachfolgenden Spielläufen kann es sich in einem dieser Zustände befinden.

  • Es wurde kein Spiel gestartet, oder das Spiel liegt zwischen Leveln (der Highscore ist null).
  • Die Spielschleife wird ausgeführt und befindet sich in der Mitte eines Levels.
  • Die Spielschleife wird aufgrund eines abgeschlossenen Spiels nicht ausgeführt (der Highscore hat einen Wert ungleich Null).

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

Spielzustandsverwaltung

Während der Initialisierung des Spiels müssen Sie also das Kaltstarten des Spiels unterstützen und das Spiel nach dem Beenden des Spiels fortsetzen. Das Simple3DGameDX-Beispiel speichert den Spielzustand immer, um den Eindruck zu erwecken, dass es nie angehalten wurde.

Als Reaktion auf ein Anhalteereignis wird das Spiel angehalten, aber die Ressourcen des Spiels befinden sich weiterhin im Arbeitsspeicher. Ebenso wird das Fortsetzungsereignis behandelt, um sicherzustellen, dass das Beispielspiel in dem Zustand aufgenommen wird, in dem es sich befand, als es angehalten oder beendet wurde. Je nach Zustand werden dem Spieler verschiedene Optionen angezeigt.

  • Wenn das Spiel auf mittlerem Level fortgesetzt wird, wird es angehalten, und das Overlay bietet die Möglichkeit, den Vorgang fortzusetzen.
  • 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 vor dem Start eines Levels fortgesetzt wird, stellt das Overlay dem Benutzer eine Startoption dar.

Das Beispielspiel unterscheidet nicht, ob das Spiel kalt gestartet, zum ersten Mal ohne Anhalteereignis gestartet oder aus einem angehaltenen Zustand fortgesetzt wird. Dies ist das richtige Design 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 ist ein Flussdiagramm, mit dem Sie den Fluss visualisieren können. Sie deckt sowohl die Initialisierung als auch die Updateschleife ab.

  • Die Initialisierung beginnt beim Überprüfen auf den aktuellen Spielzustand am Startknoten . Spielcode finden Sie im nächsten Abschnitt unter GameMain::InitializeGameState.

der Hauptzustandsautomat für unser Spiel

Die GameMain::InitializeGameState-Methode

Die GameMain::InitializeGameState-Methode wird indirekt über den Konstruktor der GameMain-Klasse aufgerufen. Dies ist das Ergebnis einer GameMain-Instanz innerhalb von App::Load.

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 Spielengine

Die App::Run-Methode ruft GameMain::Run auf. In GameMain::Run ist ein einfacher Zustandsautomat für die Behandlung aller wichtigen Aktionen, die ein Benutzer ausführen kann. Die höchste Stufe dieses Zustandsautomaten beschäftigt sich mit dem Laden eines Spiels, dem Spielen eines bestimmten Levels 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, kann nicht übergehen, bis Ressourcen (insbesondere Grafikressourcen) verfügbar sind. Wenn die asynchronen Vorgänge zum Laden von Ressourcen abgeschlossen sind, aktualisieren wir den Status auf "UpdateEngineState::ResourcesLoaded". Dies geschieht in der Regel 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 und wartet auf bestimmte Benutzereingaben. Bei dieser Eingabe handelt es sich um eine Spieleraktion zum Laden eines Spiels, zum Starten eines Levels oder zum Fortsetzen eines Levels. Der Beispielcode bezieht sich über die PressResultState-Aufzählung auf diese Unterzustände.
  3. UpdateEngineState::D ynamics. Die Spielschleife wird ausgeführt, während der Benutzer spielt. Während der Spieler spielt, sucht das Spiel nach drei Bedingungen, auf die es übertragen kann:
  • GameState::TimeExpired. Ablauf des Zeitlimits für eine Ebene.
  • GameState::LevelComplete. Abschluss eines Levels durch den Spieler.
  • GameState::GameComplete. Abschluss aller Levels durch den Spieler.

Ein Spiel ist einfach ein Zustandsautomat mit mehreren kleineren Zustandsautomaten. 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. Laden von Grafikressourcen) basieren.

Berücksichtigen Sie bei der Planung ihres Spiels die Erstellung des gesamten Spielflusses, um sicherzustellen, dass Sie alle möglichen Aktionen behandelt haben, die der Benutzer oder das System ausführen kann. Ein Spiel kann sehr kompliziert sein, so dass ein Zustandsautomat ein leistungsfähiges Tool ist, das Ihnen dabei hilft, diese Komplexität zu visualisieren und zu verwaltbarer zu machen.

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

Die GameMain::Update-Methode

Dies ist die Struktur des Zustandsautomaten, der zum Aktualisieren der Spielengine 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 verfügen und den Spielzustand abhängig von den Aktionen des Spielers und den Regeln, die das Spiel definieren, ändern. Viele Spiele, einschließlich dieses Beispielspiels, verwenden häufig Ui-Elemente (User Interface), um diese Informationen für den Spieler darzustellen. Die Benutzeroberfläche enthält Darstellungen des Spielzustands und anderer spielspezifischer Informationen wie Score, Munition oder die Anzahl der verbleibenden Chancen. Die Benutzeroberfläche wird auch als Overlay bezeichnet, da sie separat von der Hauptgrafikpipeline gerendert und über der 3D-Projektion platziert wird.

Einige UI-Informationen werden auch als Head-up-Display (HUD) dargestellt, damit der Benutzer diese Informationen sehen kann, ohne die Augen vollständig vom Hauptspielbereich zu entfernen. Im Beispielspiel erstellen wir diese Überlagerung mithilfe der Direct2D-APIs. Alternativ können wir diese Überlagerung mit XAML erstellen, die wir im Erweitern des Beispielspiels besprechen.

Es gibt zwei Komponenten für die Benutzeroberfläche.

  • Die HUD, die die Bewertung und Informationen zum aktuellen Zustand des Spiels enthält.
  • Die Pausenbitmap, bei der es sich um ein schwarzes Rechteck mit Text handelt, der während des angehaltenen/angehaltenen Zustands des Spiels überlagert ist. Dies ist die Spielüberlagerung. Wir besprechen es weiter beim Hinzufügen einer Benutzeroberfläche.

Nicht überraschend hat die Überlagerung auch einen Zustandsautomaten. Das Overlay kann einen Levelstart oder eine Meldung zum Spielende anzeigen. Es handelt sich im Wesentlichen um eine Canvas, auf der alle Informationen zum Spielzustand ausgegeben werden können, die dem Spieler angezeigt werden sollen, während das Spiel angehalten oder angehalten wird.

Das gerenderte Overlay kann je nach Zustand des Spiels einer dieser sechs Bildschirme sein.

  1. Statusbildschirm zum Laden von Ressourcen am Anfang des Spiels.
  2. Bildschirm "Spielstatistiken".
  3. Bildschirm "Startnachricht abgleichen".
  4. Game over screen when all of the levels are completed without the time running out.
  5. Game over screen when time runs out.
  6. Menübildschirm anhalten.

Durch das Trennen der Benutzeroberfläche von der Grafikpipeline Ihres Spiels können Sie unabhängig von der Grafikrendering-Engine des Spiels daran arbeiten und die Komplexität des Codes Ihres Spiels erheblich verringern.

Hier erfahren Sie, wie das Beispielspiel den Zustandsautomaten 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 die Grafikentwicklung starten.

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

Dies sind die ereignishandler, die in diesem Beispiel verwendet werden, und die Ereignisse, die sie behandeln.

Ereignishandler Beschreibung
OnActivated Behandelt CoreApplicationView::Activated. Die Spiel-App wurde in den Vordergrund gebracht, sodass das Hauptfenster aktiviert wird.
OnDpiChanged Behandelt Graphics::D isplay::D isplayInformation::D piChanged. Der DPI-Wert der Anzeige wurde geändert, und das Spiel passt seine Ressourcen entsprechend an.
Hinweis: CoreWindow-Koordinaten befinden sich in geräteunabhängigen Pixeln (DIPs) für Direct2D. Daher müssen Sie Direct2D über die Änderung in DPI benachrichtigen, damit alle 2D-Ressourcen oder Grundtypen korrekt angezeigt werden.
OnOrientationChanged Behandelt Graphics::D isplay::D isplayInformation::OrientationChanged. Die Ausrichtung der Anzeigeänderungen und des Renderings muss aktualisiert werden.
OnDisplayContentsInvalidated Behandelt Graphics::D isplay::D isplayInformation::D isplayContentsInvalidated. Für die Anzeige ist ein Neurast erforderlich, und Ihr Spiel muss erneut gerendert werden.
OnResuming Behandelt CoreApplication::Resuming. Die Spiele-App stellt das Spiel aus einem angehaltenen Zustand wieder her.
OnSuspending Behandelt CoreApplication::Suspending. Die Spiele-App speichert ihren Zustand auf dem Datenträger. Es hat 5 Sekunden Zeit, um den Zustand im Speicher zu speichern.
OnVisibilityChanged Behandelt CoreWindow::VisibilityChanged. Die Spiele-App hat die Sichtbarkeit geändert und wurde entweder sichtbar oder durch eine andere App unsichtbar gemacht.
OnWindowActivationChanged Behandelt CoreWindow::Activated. Das Hauptfenster der Spiele-App wurde deaktiviert oder aktiviert, sodass sie den Fokus entfernen und das Spiel anhalten oder den Fokus wiedererlangen muss. In beiden Fällen gibt das Overlay an, dass das Spiel angehalten wird.
OnWindowClosed Behandelt CoreWindow::Closed. Die Spiele-App schließt das Hauptfenster und hält das Spiel an.
OnWindowSizeChanged Behandelt CoreWindow::SizeChanged. Die Spiele-App zuordnen die Grafikressourcen und das Overlay neu, um die Größenänderung zu berücksichtigen, und aktualisiert dann das Renderziel.

Nächste Schritte

In diesem Thema haben wir gesehen, wie der gesamte Spielfluss mithilfe von Spielzuständen verwaltet wird und dass ein Spiel aus mehreren verschiedenen Zustandsautomaten besteht. Darüber hinaus haben wir erfahren, wie Sie die Benutzeroberfläche aktualisieren und wichtige App-Ereignishandler verwalten. Jetzt sind wir bereit, in die Renderingschleife, das Spiel und seine Mechanik einzutauchen.

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