Spielaktionen, die mehrere Frames benötigen

20

Ich habe noch nie wirklich viel Spielprogrammierung gemacht, ziemlich unkomplizierte Frage.

Stellen Sie sich vor, ich baue ein Tetris-Spiel, dessen Hauptschleife ungefähr so ​​aussieht.

for every frame
    handle input
    if it's time to make the current block move down a row
        if we can move the block
            move the block
        else
            remove all complete rows
            move rows down so there are no gaps
            if we can spawn a new block
                spawn a new current block
            else
                game over

Bisher geschieht alles im Spiel sofort - Dinge werden sofort erzeugt, Zeilen werden sofort entfernt usw. Aber was ist, wenn ich nicht möchte, dass Dinge sofort geschehen (dh Dinge animieren)?

for every frame
    handle input
    if it's time to make the current block move down a row
        if we can move the block
            move the block
        else
            ?? animate complete rows disappearing (somehow, wait over multiple frames until the animation is done)
            ?? animate rows moving downwards (and again, wait over multiple frames)
            if we can spawn a new block
                spawn a new current block
            else
                game over

In meinem Pong-Klon war dies kein Problem, da ich in jedem Frame nur den Ball bewegte und nach Kollisionen suchte.

Wie kann ich mich um dieses Problem kümmern? Sicherlich beinhalten die meisten Spiele eine Aktion, die mehr als einen Frame benötigt, und andere Dinge werden angehalten, bis die Aktion abgeschlossen ist.


quelle

Antworten:

11

Die traditionelle Lösung hierfür ist eine endliche Zustandsmaschine, die in mehreren Kommentaren vorgeschlagen wird.

Ich hasse Finite-State-Maschinen.

Sicher, sie sind einfach, sie werden in jeder Sprache unterstützt, aber es ist unglaublich, mit ihnen zu arbeiten. Jede Manipulation erfordert eine Menge fehlerhaften Code zum Kopieren und Einfügen, und das Optimieren des Effekts auf kleine Weise kann eine große Änderung des Codes bedeuten.

Wenn Sie eine Sprache verwenden können, die diese unterstützt, empfehle ich Coroutinen. Sie lassen Sie Code schreiben, der so aussieht:

function TetrisPieceExplosion()
  for brightness = 0, 1, 0.2 do
    SetExplosionBrightness(brightness)
    coroutine.yield()
  end

  AllowNewBlockToFall()

  SpawnABunchOfParticles()

  RemoveBlockPhysics()

  for transparency = 0, 1, 0.5 do
    SetBlockTransparency(transparency)
    coroutine.yield()
  end

  RemoveBlockGraphics()
end

Offensichtlich eher Pseudocodey, aber es sollte klar sein, dass dies nicht nur eine einfache lineare Beschreibung des Spezialeffekts ist, sondern es uns auch leicht ermöglicht, einen neuen Block abzulegen, während die Animation noch fertig ist . Dies mit einer Zustandsmaschine zu erreichen, wird im Allgemeinen furchtbar sein.

Nach meinem besten Wissen ist diese Funktionalität in C, C ++, C #, Objective C oder Java nicht einfach verfügbar. Dies ist einer der Hauptgründe, warum ich Lua für all meine Spielelogik benutze :)

ZorbaTHut
quelle
Sie könnten etwas in diese Richtung auch in anderen OOP-Sprachen implementieren. Stellen Sie sich eine Art ActionKlasse und eine Reihe von Aktionen vor, die ausgeführt werden sollen. Wenn eine Aktion abgeschlossen ist, entfernen Sie sie aus der Warteschlange und führen Sie die nächste Aktion usw. aus.
Bummzack
3
Das funktioniert, aber dann wollen Sie für jede einzelne Aktion von Action ableiten. Es wird auch davon ausgegangen, dass Ihr Prozess gut in eine Warteschlange passt. Wenn Sie Verzweigungen oder Schleifen mit undefinierten Endbedingungen wünschen, bricht die Warteschlangenlösung schnell zusammen. Es ist auf jeden
Fall
Stimmt, aber für das Tetris-Beispiel sollte es ausreichen :)
bummzack
Co-Routinen rocken - aber Lua als Sprache nervt auf so viele andere Arten, ich kann es einfach nicht empfehlen.
DeadMG
Solange Sie nur auf der obersten Ebene (und nicht bei einem verschachtelten Funktionsaufruf) nachgeben müssen, können Sie dasselbe in C # erreichen, aber ja, Lua-Coroutinen rocken.
Munificent
8

Ich übernehme dies aus Game Coding Complete von Mike McShaffry.

Er spricht von einem „Prozessmanager“, der sich auf eine Liste von Aufgaben beschränkt, die erledigt werden müssen. Beispielsweise würde ein Prozess die Animation zum Zeichnen eines Schwertes (AnimProcess) oder zum Öffnen einer Tür steuern oder in Ihrem Fall die Zeile verschwinden lassen.

Der Prozess wird zur Liste des Prozessmanagers hinzugefügt, die bei jedem Frame und jedem aufgerufenen Update () iteriert wird. Also sehr ähnliche Entitäten, aber für Aktionen. Es gibt ein Kill-Flag, das aus der Liste entfernt werden muss, wenn es beendet ist.

Das andere nette an ihnen ist, wie sie sich verbinden können, indem sie einen Zeiger auf den nächsten Prozess haben. Auf diese Weise kann Ihr animierter Zeilenprozess tatsächlich bestehen aus:

  • Ein Animationsprozess für die Zeile verschwindet
  • Ein MovementProcess zum Entfernen der Teile
  • Ein ScoreProcess, um Punkte zum Score hinzuzufügen

(Da Prozesse Dinge sein können, die nur einmal verwendet werden können, bedingt dort oder für eine bestimmte Zeitspanne)

Wenn Sie weitere Informationen wünschen, wenden Sie sich an uns.

Die kommunistische Ente
quelle
3

Sie können eine Prioritätswarteschlange von Aktionen verwenden. Sie drücken eine Aktion und eine Zeit ein. In jedem Frame erhalten Sie die Zeit, und Sie entfernen alle Aktionen, für die eine Zeit angegeben wurde, und führen sie aus. Bonus: Der Ansatz verläuft gut parallel und Sie können praktisch die gesamte Spiellogik auf diese Weise implementieren.

DeadMG
quelle
1

Sie müssen immer den Zeitunterschied zwischen dem vorherigen und dem aktuellen Frame kennen, dann müssen Sie zwei Dinge tun.

-Entscheiden Sie, wann Sie Ihr Modell aktualisieren möchten: z. Wenn in Tetris eine Zeilenentfernung beginnt, möchten Sie nicht, dass Inhalte mehr mit der Zeile kollidieren. Entfernen Sie daher die Zeile aus dem 'Modell' Ihrer Anwendung.

-Sie müssen dann das Objekt, das sich in einem Übergangszustand befindet, in eine separate Klasse umwandeln, die die Animation / das Ereignis über einen bestimmten Zeitraum auflöst. Im Tetris-Beispiel müsste die Zeile langsam ausgeblendet werden (ändern Sie die Opazität für jedes Bild ein wenig). Nachdem die Undurchsichtigkeit 0 ist, werden alle Blöcke oben in der Reihe um eins nach unten verschoben.

Dies mag auf den ersten Blick etwas kompliziert erscheinen, aber Sie werden den Dreh rausfinden. Stellen Sie einfach sicher, dass Sie in verschiedenen Klassen viel abstrahieren. Dies wird es einfacher machen. Stellen Sie außerdem sicher, dass zeitaufwändige Ereignisse wie das Entfernen einer Zeile in Tetris vom Typ "Fire and Forget" (Feuer und Vergessen) sind. Erstellen Sie einfach ein neues Objekt, das alles behandelt, was automatisch erledigt werden muss, und das, wenn alles erledigt ist. Entfernt sich von Ihrem Szenenbild.

Roy T.
quelle
In einigen Fällen können auch schwere Berechnungen die zulässige Zeit für einen physikalischen Zeitschritt überschreiten (z. B. Kollisionserkennung und Bahnplanung). In diesen Fällen können Sie aus den Berechnungen springen, wenn die zugewiesene Zeit verwendet wurde, und die Berechnung im nächsten Frame fortsetzen.
Nagler
0

Sie müssen sich das Spiel als "endliche Zustandsmaschine" vorstellen. Das Spiel kann sich in einem von mehreren Zuständen befinden: in Ihrem Fall "Eingabe erwarten", "Teil nach unten bewegen", "Reihe explodieren".

Sie tun je nach Bundesstaat unterschiedliche Dinge. Beispielsweise ignorieren Sie während des "Abwärtsfahrens" die Eingaben des Spielers und animieren stattdessen das Stück von seiner aktuellen Reihe in die nächste Reihe. Etwas wie das:

if state == ACCEPTING_INPUT:
    if player presses any key:
        handle input
    row_timer = row_timer - time_since_last_frame
    if row_timer < 0:
        state = MOVING_PIECE_DOWN
elif state == MOVING_PIECE_DOWN:
    piece.y = piece.y + piece.speed*time_since_last_frame
    if piece.y >= target_piece_y:
        piece.y = target_piece_y
        state = ACCEPTING_INPUT
ggambett
quelle