Definieren des Hauptobjekts für das Spiel

Hinweis

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

Nachdem Sie das grundlegende Framework des Beispielspiels festgelegt und einen Zustandsautomat implementiert haben, der das allgemeine Benutzer- und Systemverhalten verarbeitet, sollten Sie die Regeln und Mechaniken untersuchen, die das Beispielspiel in ein Spiel verwandeln. Sehen wir uns die Details des Standard-Objekts des Beispielspiels an und wie Spielregeln in Interaktionen mit der Spielwelt übersetzt werden.

Ziele

  • Erfahren Sie, wie Sie grundlegende Entwicklungstechniken anwenden, um Spielregeln und -mechaniken für ein UWP DirectX-Spiel zu implementieren.

Hauptobjekt für das Spiel

Im Simple3DGameDX-Beispielspiel ist Simple3DGame die Standard Spielobjektklasse. Eine instance von Simple3DGame wird indirekt über die App::Load-Methode erstellt.

Hier sind einige der Features der Simple3DGame-Klasse .

  • Enthält die Implementierung der Spiellogik.
  • Enthält Methoden, die diese Details kommunizieren.
    • Ändert den Spielzustand an den zustandsautomat, der im App-Framework definiert ist.
    • Ändert sich im Spielzustand von der App zum Spielobjekt selbst.
    • Details zum Aktualisieren der Benutzeroberfläche des Spiels (Überlagerung und Head-up-Anzeige), Animationen und Physik (die Dynamik).

    Hinweis

    Das Aktualisieren von Grafiken erfolgt durch die GameRenderer-Klasse , die Methoden zum Abrufen und Verwenden von Grafikgeräteressourcen enthält, die vom Spiel verwendet werden. Weitere Informationen finden Sie unter Renderingframework I: Einführung in das Rendering.

  • Dient als Container für die Daten, die eine Spielsitzung, ein Level oder eine Lebensdauer definieren, je nachdem, wie Sie Ihr Spiel auf hoher Ebene definieren. In diesem Fall sind die Spielzustandsdaten für die Lebensdauer des Spiels und werden einmal initialisiert, wenn ein Benutzer das Spiel startet.

Informationen zum Anzeigen der von dieser Klasse definierten Methoden und Daten finden Sie weiter unten unter Die Simple3DGame-Klasse .

Initialisieren und Starten des Spiels

Wenn ein Spieler das Spiel startet, muss das Spielobjekt den eigenen Zustand initialisieren, das Overlay erstellen und hinzufügen, die Variablen zur Nachverfolgung der Erfolge des Spielers festlegen und die Objekte instanziieren, die zum Erstellen der Level benötigt werden. In diesem Beispiel erfolgt dies, wenn die GameMain-instance in App::Load erstellt wird.

Das Spielobjekt vom Typ Simple3DGame wird im GameMain::GameMain-Konstruktor erstellt. Sie wird dann mit der Simple3DGame::Initialize-Methode während der Fire-and-Forget-Coroutine gameMain::ConstructInBackground initialisiert, die von GameMain::GameMain aufgerufen wird.

Die Simple3DGame::Initialize-Methode

Das Beispielspiel richtet diese Komponenten im Spielobjekt ein.

  • Ein neues Audiowiedergabeobjekt wird erstellt.
  • Arrays für die Grafikgrundtypen des Spiels werden erstellt (einschließlich Arrays für die Levelgrundtypen, Munition und Hindernisse).
  • Ein Speicherort für die Spielzustandsdaten mit der Bezeichnung Game wird erstellt und am durch ApplicationData::Current festgelegten Speicherort der App-Dateneinstellungen platziert.
  • Ein Spieltimer und die anfängliche spielinterne Overlaybitmap werden erstellt.
  • Eine neue Kamera mit einem spezifischen Satz von Ansichts- und Projektionsparametern wird erstellt.
  • Das Eingabegerät (Controller) wird auf die gleiche Ausgangsausrichtung festgelegt wie die Kamera, sodass die Ausgangsposition der Steuerung exakt der Kameraposition entspricht.
  • Das Spielerobjekt wird erstellt und aktiviert. Wir verwenden ein Kugelobjekt, um die Nähe des Spielers zu Wänden und Hindernissen zu erkennen und die Kamera nicht in eine Position zu setzen, die das Eintauchen unterbrechen könnte.
  • Der Spielweltgrundtyp wird erstellt.
  • Die Zylinderhindernisse werden erstellt.
  • Die Ziele (Face-Objekte) werden erstellt und nummeriert.
  • Die Munitionskugeln werden erstellt.
  • Die Level werden erstellt.
  • Der Highscore wird geladen.
  • Alle ggf. zuvor gespeicherten Spielzustände werden geladen.

Das Spiel verfügt jetzt über Instanzen aller wichtigen Komponenten – die Welt, den Spieler, die Hindernisse, die Ziele und die Munitionssphären. Außerdem besitzt es Instanzen der Level. Diese stehen für Konfigurationen aller oben genannten Komponenten und ihrer Verhalten für die einzelnen spezifischen Level. Nun sehen wir uns an, wie das Spiel die Levels aufbaut.

Erstellen und Laden von Spielebenen

Der größte Teil der schweren Arbeit für die Ebenenkonstruktion erfolgt in den Level[N].h/.cpp Dateien, die sich im Ordner GameLevels der Beispiellösung befinden. Da es sich auf eine sehr spezifische Implementierung konzentriert, werden wir sie hier nicht behandeln. Wichtig ist, dass der Code für jede Ebene als separates Level[N] -Objekt ausgeführt wird. Wenn Sie das Spiel erweitern möchten, können Sie ein Level[N] -Objekt erstellen, das eine zugewiesene Zahl als Parameter akzeptiert und die Hindernisse und Ziele zufällig platziert. Sie können auch Konfigurationsdaten der Ebene aus einer Ressourcendatei oder sogar aus dem Internet laden.

Definieren des Gameplays

An diesem Punkt verfügen wir über alle Komponenten, die wir benötigen, um das Spiel zu entwickeln. Die Ebenen wurden im Arbeitsspeicher aus den Grundtypen erstellt und sind für den Spieler bereit, mit der Interaktion zu beginnen.

Die besten Spiele reagieren sofort auf Spielereingaben und geben sofortiges Feedback. Dies gilt für jede Art von Spiel, von Twitch-Action, Echtzeit-Ego-Shootern bis hin zu durchdachten, rundenbasierten Strategiespielen.

Die Simple3DGame::RunGame-Methode

Während ein Spiellevel ausgeführt wird, befindet sich das Spiel im Dynamics-Zustand .

GameMain::Update ist die Standard Updateschleife, die den Anwendungsstatus einmal pro Frame aktualisiert, wie unten gezeigt. Die Updateschleife ruft die Simple3DGame::RunGame-Methode auf, um die Arbeit zu verarbeiten, wenn sich das Spiel im Dynamics-Zustand befindet.

// Updates the application state once per frame.
void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update();

    switch (m_updateState)
    {
    ...
    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)
            {
                ...

Simple3DGame::RunGame verarbeitet den Datensatz, der den aktuellen Zustand des Spiels für die aktuelle Iteration der Spielschleife definiert.

Hier ist die Spielflusslogik in Simple3DGame::RunGame.

  • Die -Methode aktualisiert den Timer, der die Sekunden nach unten zählt, bis die Ebene abgeschlossen ist, und testet, ob die Zeit der Ebene abgelaufen ist. Dies ist eine der Regeln des Spiels – wenn die Zeit abgelaufen ist, wenn nicht alle Ziele geschossen wurden, dann ist das Spiel vorbei.
  • Wenn die Zeit abgelaufen ist, legt die Methode den TimeExpired-Spielzustand fest und kehrt zur Update-Methode im vorherigen Code zurück.
  • Wenn die Zeit verbleibt, wird der Move-Look-Controller für eine Aktualisierung der Kameraposition abgefragt. insbesondere eine Aktualisierung des Winkels der Ansicht, die normal projiziert wird von der Kameraebene (wo der Spieler sucht), und die Entfernung, die sich seit der letzten Abfrage des Controllers bewegt hat.
  • Die Kamera wird auf der Grundlage der neuen Daten des Bewegungs- und Blickrichtungscontrollers aktualisiert.
  • Die Dynamik (also die Animationen und das Verhalten von Objekten in der Spielwelt, die nicht vom Spieler gesteuert werden) wird aktualisiert. In diesem Beispielspiel wird die Simple3DGame::UpdateDynamics-Methode aufgerufen, um die Bewegung der abgefeuerten Munitionskugeln, die Animation der Säulenhindernisse und die Bewegung der Ziele zu aktualisieren. Weitere Informationen finden Sie unter Aktualisieren der Spielwelt.
  • Die -Methode überprüft, ob die Kriterien für den erfolgreichen Abschluss einer Ebene erfüllt sind. Wenn dies der Fall ist, wird die Bewertung für die Stufe abgeschlossen und überprüft, ob dies die letzte Ebene (von 6) ist. Wenn es sich um die letzte Ebene handelt, gibt die Methode den GameState::GameComplete-Spielzustand zurück. Andernfalls wird der GameState::LevelComplete-Spielzustand zurückgegeben.
  • Wenn die Ebene nicht vollständig ist, legt die Methode den Spielzustand auf GameState::Active fest und gibt zurück.

Aktualisieren der Spielwelt

Wenn das Spiel ausgeführt wird, wird in diesem Beispiel die Simple3DGame::UpdateDynamics-Methode von der Simple3DGame::RunGame-Methode (die von GameMain::Update aufgerufen wird) aufgerufen, um Objekte zu aktualisieren, die in einer Spielszene gerendert werden.

Eine Schleife wie UpdateDynamics ruft alle Methoden auf, die verwendet werden, um die Spielwelt unabhängig von der Spielereingabe in Bewegung zu setzen, um ein immersives Spielerlebnis zu schaffen und das Level lebendig zu machen. Dies umfasst Grafiken, die gerendert werden müssen, und das Ausführen von Animationsschleifen, um eine dynamische Welt zu bewirken, auch wenn keine Spielereingabe vorhanden ist. In Ihrem Spiel könnte dies Bäume sein, die sich im Wind wiegen, Wellen entlang der Küstenlinien, Maschinen rauchen und außerirdische Monster, die sich strecken und bewegen. Hierzu gehört auch die Interaktion zwischen Objekten (beispielsweise Kollisionen zwischen der Spielerkugel und der Welt oder zwischen der Munition und den Hindernissen oder Zielen).

Außer wenn das Spiel speziell angehalten wird, sollte die Spielschleife die Spielwelt weiterhin aktualisieren. ob dies auf Spiellogik, physischen Algorithmen oder einfach zufällig basiert.

Im Beispielspiel wird dieses Prinzip als Dynamik bezeichnet und umfasst den Anstieg und Fall der Säulenhindernisse sowie die Bewegung und das physische Verhalten der Munitionssphären, während sie geschossen und in Bewegung sind.

Die Simple3DGame::UpdateDynamics-Methode

Diese Methode behandelt diese vier Sätze von Berechnungen.

  • Die Positionen der Kugeln für die abgefeuerte Munition in der Spielwelt.
  • Die Animation der Säulenhindernisse.
  • Die Überschneidung des Spielers mit den Begrenzungen der Spielwelt.
  • Die Kollisionen von Munitionskugeln mit den Hindernissen, den Zielen, anderen Munitionskugeln und der Spielwelt.

Die Animation der Hindernisse erfolgt in einer Schleife, die in den Quellcodedateien animate.h/.cpp definiert ist. Das Verhalten der Munition und alle Kollisionen werden durch vereinfachte Physikalgorithmen definiert, die im Code bereitgestellt und durch eine Reihe von globalen Konstanten für die Spielwelt parametrisiert werden, einschließlich Schwerkraft und Materialeigenschaften. All dies wird im Koordinatenbereich der Spielwelt berechnet.

Überprüfen des Flows

Nachdem wir nun alle Objekte in der Szene aktualisiert und alle Kollisionen berechnet haben, müssen wir diese Informationen verwenden, um die entsprechenden visuellen Änderungen zu zeichnen.

Nachdem GameMain::Update die aktuelle Iteration der Spielschleife abgeschlossen hat, ruft das Beispiel sofort GameRenderer::Render auf, um die aktualisierten Objektdaten zu übernehmen und eine neue Szene zu generieren, die dem Spieler präsentiert werden soll, wie unten gezeigt.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                ...
                // Otherwise, fall through and do normal processing to perform rendering.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                    CoreProcessEventsOption::ProcessAllIfPresent);
                // GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
                // state, uses Simple3DGame::UpdateDynamics to update game world.
                Update();
                // Render is called immediately after the Update loop.
                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.
}

Rendern der Grafiken der Spielwelt

Es wird empfohlen, die Grafik in einem Spiel häufig zu aktualisieren, idealerweise genau so oft, wie die Standard Spielschleife durchläuft. Während die Schleife durchlaufen wird, wird der Zustand der Spielwelt mit oder ohne Spielereingabe aktualisiert. Dadurch können die berechneten Animationen und Verhaltensweisen reibungslos angezeigt werden. Stellen Sie sich vor, wir hätten eine einfache Wasserszene, die sich nur bewegte, wenn der Spieler eine Taste drückte. Das wäre nicht realistisch; ein gutes Spiel sieht die ganze Zeit glatt und flüssig aus.

Erinnern Sie sich an die Schleife des Beispielspiels, wie oben in GameMain::Run gezeigt. Wenn das Standard Fenster des Spiels sichtbar ist und nicht angedockt oder deaktiviert ist, wird das Spiel weiterhin aktualisiert und die Ergebnisse dieses Updates gerendert. Die GameRenderer::Render-Methode , die als Nächstes untersucht wird, rendert eine Darstellung dieses Zustands. Dies erfolgt unmittelbar nach einem Aufruf von GameMain::Update, der Simple3DGame::RunGame enthält, um Zustände zu aktualisieren, wie im vorherigen Abschnitt erläutert.

GameRenderer::Render zeichnet die Projektion der 3D-Welt und zeichnet dann die Direct2D-Überlagerung darüber. Nach Abschluss des Vorgangs zeigt sie die finale Swapchain mit den kombinierten Puffern an.

Hinweis

Es gibt zwei Zustände für das Direct2D-Overlay des Beispielspiels: einer, in dem das Spiel das Spielinfo-Overlay anzeigt, das die Bitmap für das Pausenmenü enthält, und einer, in dem das Spiel das Fadenkreuz zusammen mit den Rechtecken für den Touchscreen-Move-Look-Controller anzeigt. Der Text mit dem Spielstand wird in beiden Zuständen gezeichnet. Weitere Informationen finden Sie unter Renderingframework I: Einführung in das Rendering.

Die GameRenderer::Render-Methode

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    ...
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            ...
            for (auto&& object : m_game->RenderObjects())
            {
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
        d2dContext->BeginDraw();

        // To handle the swapchain being pre-rotated, set the D2D transformation to include it.
        d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());

        if (m_game != nullptr && m_gameResourcesLoaded)
        {
            // This is only used after the game state has been initialized.
            m_gameHud.Render(m_game);
        }

        if (m_gameInfoOverlay.Visible())
        {
            d2dContext->DrawBitmap(
                m_gameInfoOverlay.Bitmap(),
                m_gameInfoOverlayRect
                );
        }
        ...
    }
}

Die Simple3DGame-Klasse

Dies sind die Methoden und Datenmember, die von der Simple3DGame-Klasse definiert werden.

Memberfunktionen

Zu den von Simple3DGame definierten öffentlichen Memberfunktionen gehören die folgenden Funktionen.

  • Initialisieren. Legt die Startwerte der globalen Variablen fest und initialisiert die Spielobjekte. Dies wird im Abschnitt Initialisieren und Starten des Spiels behandelt.
  • LoadGame. Initialisiert eine neue Ebene und beginnt mit dem Laden.
  • LoadLevelAsync. Eine Coroutine, die die Ebene initialisiert und dann eine weitere Coroutine auf dem Renderer aufruft, um die gerätespezifischen Ebenenressourcen zu laden. Diese Methode wird in einem gesonderten Thread ausgeführt. Daher können in diesem Thread nur ID3D11Device-Methoden (und keine ID3D11DeviceContext-Methoden) aufgerufen werden. Alle Gerätekontextmethoden werden in der FinalizeLoadLevel-Methode aufgerufen. Wenn Sie noch nicht mit der asynchronen Programmierung vertraut sind, lesen Sie Parallelität und asynchrone Vorgänge mit C++/WinRT.
  • FinalizeLoadLevel. Führt alle Aktionen zum Laden des Levels aus, die im Hauptthread durchgeführt werden müssen. Dies schließt alle Aufrufe von Direct3D 11-Gerätekontextmethoden (ID3D11DeviceContext) ein.
  • StartLevel. Startet das Gameplay für ein neues Level.
  • PauseGame. Hält das Spiel an.
  • RunGame. Führt eine Iteration der Spielschleife aus. Wird jeweils einmal pro Iteration der Spielschleife von App::Update aufgerufen, sofern sich das Spiel im Zustand Active befindet.
  • OnSuspending und OnResuming. Anhalten bzw. Fortsetzen der Audiodaten des Spiels.

Hier sind die privaten Memberfunktionen aufgeführt.

  • LoadSavedState und SaveState. Laden bzw. speichern Sie den aktuellen Zustand des Spiels.
  • LoadHighScore und SaveHighScore. Laden bzw. speichern Sie die hohe Punktzahl in allen Spielen.
  • InitializeAmmo. Setzt den Zustand der als Munition verwendeten Kugelobjekte in den Originalzustand für den Beginn einer neuen Runde zurück.
  • UpdateDynamics. Dies ist eine wichtige Methode, da sie alle Spielobjekte basierend auf dosenbasierten Animationsroutinen, Physik und Steuerungseingaben aktualisiert. Hierbei handelt es sich gewissermaßen um das Herzstück der Interaktivität, die das Spiel ausmacht. Dies wird im Abschnitt Aktualisieren der Spielwelt behandelt.

Die anderen öffentlichen Methoden sind Eigenschaftenaccessor, die gameplay- und overlayspezifische Informationen zur Anzeige an das App-Framework zurückgeben.

Datenmember

Diese Objekte werden aktualisiert, während die Spielschleife ausgeführt wird.

  • MoveLookController-Objekt . Stellt die Playereingabe dar. Weitere Informationen finden Sie unter Hinzufügen von Steuerelementen.
  • GameRenderer-Objekt . Stellt einen Direct3D 11-Renderer dar, der alle gerätespezifischen Objekte und deren Rendering verarbeitet. Weitere Informationen finden Sie unter Renderingframework I.
  • Audioobjekt . Steuert die Audiowiedergabe für das Spiel. Weitere Informationen finden Sie unter Hinzufügen von Sound.

Die restlichen Spielvariablen enthalten die Listen der Grundtypen und deren jeweiligen Spielgrößen sowie spielspezifische Daten und Einschränkungen.

Nächste Schritte

Wir müssen noch über die eigentliche Rendering-Engine sprechen– wie Aufrufe der Rendermethoden auf den aktualisierten Grundtypen auf dem Bildschirm in Pixel umgewandelt werden. Diese Aspekte werden in zwei Teilen behandelt: Renderingframework I: Einführung in das Rendering und Renderingframework II: Spielerendering. Wenn Sie mehr daran interessiert sind, wie die Spielersteuerung den Spielzustand aktualisiert, lesen Sie Hinzufügen von Steuerelementen.