Was wäre der beste Weg, um Bewegungen in einem Spiel zu speichern, um einen Rollback zu ermöglichen?

8

Ich entwickle ein Brettspiel mit einer Spielklasse, die den Spielfluss steuert, und Spielern, die der Spielklasse zugeordnet sind. Das Brett ist nur eine visuelle Klasse, aber die Kontrolle über die Bewegungen liegt ganz beim Spiel.
Der Spieler kann versuchen, falsche Bewegungen auszuführen, die dazu führen, dass das Spiel dem Spieler mitteilt, dass diese Art von Bewegung nicht zulässig ist (und den Grund dafür angeben). In einigen Fällen kann der Spieler jedoch einfach eine Bewegung ausführen und erkennen, dass dies nicht die beste Bewegung ist, oder einfach das Klicken auf das Brett verpassen oder einfach einen anderen Ansatz ausprobieren.
Das Spiel kann gespeichert und geladen werden, sodass bei einem Ansatz alle Daten vor der Bewegung gespeichert werden können und kein Rollback zulässig ist, der Benutzer jedoch die letzte automatisch gespeicherte Runde laden kann. Dies sieht nach einem netten Ansatz aus, beinhaltet jedoch eine Benutzerinteraktion, und das Neuzeichnen des Boards kann für den Benutzer mühsam sein. Gibt es einen besseren Weg, um solche Dinge zu tun, oder ist die Architektur in dieser Sache wirklich wichtig?

Die Spielklasse und die Spielerklasse sind nicht kompliziert, daher ist es eine gute Idee, die Klassen zu klonen, oder die Spieldaten von den Spielregeln zu trennen, um einen besseren Ansatz zu finden, oder das Speichern / Laden (auch automatisch bei einem angeforderten Rollback) ist in Ordnung?

UPDATE: Wie dieses Spiel funktioniert: Es hat ein Hauptbrett, auf dem Sie Züge machen (einfache Züge), und ein Spielerbrett, das auf die Züge reagiert, macht auf dem Hauptbrett. Es hat auch Reaktionsbewegungen entsprechend den Bewegungen anderer Spieler und auf sich selbst. Sie können auch reagieren, wenn Sie nicht an der Reihe sind und Dinge auf Ihrem Brett tun. Vielleicht kann ich nicht jede Bewegung rückgängig machen, aber ich mag die Idee des Rückgängigmachens / Wiederherstellens, die in einer der aktuellen Antworten schwebt.

gbianchi
quelle
4
Schauen Sie sich das Erinnerungsmuster an
1
Speichern Sie einfach den Verlauf aller Objekte in jeder Runde auf dem Brett.
Ramhound
2
@ Ramhound: Wenn Sie es so machen, könnte Ihr Programm leicht zu einem ... na ja ... RAM-Hund werden. ;)
Mason Wheeler
1
@MasonWheeler - Sie müssen den Verlauf nicht über einen bestimmten Punkt hinaus verfolgen. Viele Programme speichern Tonnen von Daten in einer ähnlichen Funktion und haben keine Probleme mit dem Speicher.
Ramhound
Können Sie die Bewegungskoordinaten auf einem Stapel speichern? Dies geschieht in Schachspielen. YMMV abhängig davon, ob es sich um ein rundenbasiertes Brettspiel oder eine fließende Bewegung wie Pong handelt.
Mike30

Antworten:

16

Warum nicht die Historie aller durchgeführten Bewegungen (sowie aller anderen nicht deterministischen Ereignisse) speichern? Auf diese Weise können Sie jederzeit einen bestimmten Spielstatus rekonstruieren.

Dies benötigt deutlich weniger Speicherplatz als das Speichern aller Spielzustände und ist relativ einfach zu implementieren.


quelle
2
Als zusätzlichen Bonus können Sie irgendwann eine Wiederholungsfunktion hinzufügen, die StarCraft ähnelt.
Jonathan Rich
2
+1 Dies ähnelt einer Technik namens Event Sourcing, die in anderen Kontexten verwendet wird.
MattDavey
1
Der Kommentar von @MattDavey Erklären Sie diese Technik auf großartige Weise. Ich werde das akzeptieren, weil es ziemlich einfach zu implementieren sein kann, egal wie komplex das Spiel ist.
Gbianchi
Event Sourcing ist der richtige Weg - bringt Sie zu jedem Punkt im Spiel
Murph
8

Das Erstellen eines Rückgängig-Systems ist konzeptionell ziemlich einfach. Sie müssen nur die Änderungen verfolgen. Sie benötigen einen Stapel und einen Objekttyp, der einen Teil des Status des Spiels beschreibt. Ihr Stapel sollte ein Stapel von Arrays / Listen von Spielstatusobjekten sein.

Der Trick ist, keine Bewegungen aufzuzeichnen, die Leute machen . Ich sehe das in den anderen Antworten, und es kann getan werden, aber es ist viel komplizierter. Notieren Sie stattdessen, wie das Spielbrett aussah, bevor der Zug ausgeführt wurde. Wenn Sie beispielsweise ein Stück verschieben, haben Sie zwei Änderungen vorgenommen: Das Stück hat ein Quadrat verlassen, und das Stück wurde auf ein neues Quadrat gelegt. Wenn Sie ein Array erstellen, das zeigt, wie diese beiden Quadrate vor Beginn des Zugs ausgesehen haben, können Sie den Zug rückgängig machen, indem Sie diese beiden Quadrate einfach so wiederherstellen, wie sie vor dem Zug waren.

Oder wenn Ihr Spielstatus in den Figuren und nicht in den Quadraten gehalten wird, zeichnen Sie die Position der Figur auf, bevor sie sich bewegt. (Und wenn Ihre Figur mit anderen Figuren interagiert, z. B. mit einer Schachaufnahme, notieren Sie, wo sich diese anderen Figuren befanden, bevor sie geändert wurden.)

Wenn eine Bewegung passiert:

  • Erstellen Sie einen neuen Undo-Frame (Array)
  • Jedes Mal, wenn sich etwas ändert und Sie noch keine Änderung für dieses Objekt im aktuellen Rückgängig-Frame haben, fügen Sie dessen Status dem aktuellen Rückgängig-Frame hinzu, bevor Sie die Änderung anwenden.
  • Wenn die Bewegung beendet ist, schieben Sie den Rückgängig-Rahmen auf den Stapel.

Wenn der Benutzer Rückgängig sagt:

  • Pop die Oberseite des Stapels und greifen Sie einen Rückgängig-Rahmen
  • Durchlaufen Sie jedes Objekt im Frame und stellen Sie es wieder her
  • (Optional): Verfolgen Sie die hier vorgenommenen Änderungen genauso wie beim Einrichten eines Rückgängig-Rahmens und schieben Sie den Rahmen auf einen Stapel. So implementieren Sie Redo. (Wenn Sie dies tun, sollte durch Drücken eines neuen Rückgängig-Rahmens auch der Wiederherstellungsstapel gelöscht werden.)
Mason Wheeler
quelle
4
Dies ist eine gute Idee, vorausgesetzt, der Status des Spiels nimmt relativ wenig Speicherplatz ein. Mein erster wirklicher Open-Source-Beitrag bestand darin, das Rückgängig-System von Cinelerra auf das Modell "Rekordbewegungen" umzustellen, da bei großen Projekten jedes Mal, wenn ich eine Taste drücke, Megabyte an Statusdaten auf den Rückgängig-Stapel verschoben werden. Es ist vielleicht nie ein Problem mit einem Brettspiel, aber wenn es eine Chance gibt, dass sich der Speicherbedarf so erhöht, ist es besser, jetzt in die Kugel zu beißen.
Karl Bielefeldt
2
@KarlBielefeldt, ich sehe diese Antwort darin, nur die Änderungen des Status zu speichern, nicht den gesamten Spielstatus an jedem Punkt. Die Antwort enthält viele nützliche Überlegungen zum Problem, aber ich mag die Eröffnung "Züge nicht aufzeichnen, wie die anderen Antworten sagen" nicht, wenn sie wirklich etwas sehr Ähnliches befürwortet.
@ dan1111: Ich befürworte etwas Ähnliches , aber subtil anderes. Wenn Sie "Zugbewegungen aufzeichnen" sagen, klingt dies wie "Änderungen aufzeichnen". Was ich beschreibe, ist "Aufzeichnen, wie Dinge ausgesehen haben, bevor sie geändert wurden", was das Rückgängig-System viel sauberer macht.
Mason Wheeler
1
@MasonWheeler, ich stimme zu, dass die Art und Weise, wie Sie die Implementierung vorschlagen, elegant ist. Für mich ist es jedoch einfach eine gute Möglichkeit, die Zustände aufzuzeichnen, die sich geändert haben . Ich nehme an, es ist wirklich nur ein Argument über Semantik ...
1
@kuhaku Nr Aufnahme der Bewegungen , die gemacht werden machen eine Aufzeichnung von dem, was Sie ändern zu . Was ich hier zu erklären ist , dass Sie eine Aufzeichnung machen müssen , was Sie ändern aus , so dass Sie es zurückbekommen kann.
Mason Wheeler
2

Eine gute Möglichkeit, dies zu implementieren, besteht darin, Ihre Bewegungen in Form von Befehlsobjekten zu kapseln. Stellen Sie sich eine Befehlsschnittstelle mit den Methoden move(Position newPosition)und vor undo. Eine konkrete Klasse kann diese Schnittstelle so implementieren, dass sie für die moveMethode die aktuelle Position auf der Platine speichern kann (Position ist eine Klasse, die wahrscheinlich die Zeilen- und Spaltenwerte enthält) und dann eine Bewegung zu der durch identifizierten Zeile und Spalte ausführen kann newPosition. Danach fügt die Methode den Befehl ( this) einem globalen Stapel von Befehlsobjekten hinzu.

Wenn der Benutzer nun zum vorherigen Schritt zurückkehren muss, entfernen Sie einfach die letzte Befehlsinstanz vom Stapel und rufen Sie ihre undoMethode auf. Auf diese Weise können Sie auch auf so viele Schritte zurückgreifen, wie Sie benötigen.

Ich hoffe, das hilft.

Samir Hasan
quelle
2
Command ist ein klassisches Designmuster und genau das, was ich vorschlagen wollte. Außerdem sollte jeder Befehl einen Zeiger auf den Spieler haben, der den Zug ausgeführt hat, damit Sie am Ende eine Abschrift des gesamten Spiels erhalten, indem Sie einfach die Liste durchlaufen und für jeden Befehl eine Funktion vom Typ "toString" aufrufen. Beispiel: "Annie hat einen Gelegenheitskunden (Weizen, Kürbis, Rübe) und einen Helfer (Käufer) hinzugefügt. Die Rechnung wird an den Stammkunden (Weizen, Kürbis) für +8 Bargeld geliefert", lautet die Beschreibung für ein paar Spieler, die sich melden "Vor den Toren von Loyang".
John Munsch
Netter Vorschlag John.
Samir Hasan
1

Ein alter Thread wird gestoßen, aber der einfachste Weg, dieses Problem zu lösen, besteht zumindest in komplexen Szenarien darin, einfach alles zu kopieren, bevor der Benutzer arbeitet ...

... klingt verschwenderisch, oder? Außer wenn es wirklich verschwenderisch ist, besteht Ihr nächster Schritt darin, das Kopieren billiger zu machen, damit Teile, die im nächsten Schritt nicht geändert werden, nicht tief kopiert werden.

In meinem Fall arbeite ich oft mit Gigabyte an Daten, aber der Benutzer berührt oft nur Megabyte für eine Operation, so dass das Kopieren von allem normalerweise extrem teuer und lächerlich verschwenderisch wäre. Aber ich habe diese Datenstrukturen eingerichtet, die sich um flache Daten drehen Kopieren, was sich nicht ändert, und ich finde, dass dies die einfachste Lösung ist, und es eröffnet auch viele neue Möglichkeiten, wie unveränderliche persistente Datenstrukturen, sichereres Multithreading, Knotennetzwerke, die etwas eingeben und etwas Neues ausgeben, zerstörungsfreie Bearbeitung, Instanzierung Objekte (die beispielsweise ihre teuren Netzdaten flach kopieren können, aber den Klon haben, haben ihre eigene einzigartige Position in der Welt, während sie kaum Speicherplatz beanspruchen) usw.

store all scene/application state in undo
perform user operation

on undo/redo:
   swap stored state with application state

Jedes Projekt ist seitdem diesem Grundmuster gefolgt, anstatt jede kleine Zustandsänderung sorgfältig aufzeichnen zu müssen und für Dutzende verschiedener Datentypen, die nicht in ein homogenes Modell passen (Bild- / Texturmalerei, Eigenschaftsänderungen, Netzänderungen, Gewichtsänderungen, hierarchische Szenenänderungen, Shader-Änderungen, die nicht eigenschaftsbezogen waren, wie das Austauschen untereinander, Umschlagänderungen usw. usw. usw.). Wenn dies in Bezug auf Speicher und Zeit zu verschwenderisch ist, entwickeln wir kein ausgefalleneres Rückgängig-System, sondern versuchen, das Kopieren so zu optimieren, dass Teilkopien in den teuersten Datenstrukturen erstellt werden können. Damit bleibt es ein Optimierungsdetail, bei dem Sie sofort eine korrekt funktionierende Rückgängig-Implementierung haben können, die immer korrekt bleibt, und immer korrekt zu bleiben, ist fantastisch, wenn in der Vergangenheit

Die andere Möglichkeit besteht darin, die Deltas (Änderungen) einzeln aufzuzeichnen, und das habe ich früher getan, aber für sehr komplexe Großsoftware war es oft sehr fehleranfällig und langwierig, da es für einen Plugin-Entwickler sehr einfach war um zu vergessen, dass einige Änderungen im Rückgängig-Stapel aufgezeichnet wurden, als brandneue Konzepte in das System eingeführt wurden.

Unabhängig davon, ob Sie den gesamten Spielstatus kopieren oder Deltas aufzeichnen, besteht eine Sache darin, Zeiger (unter der Annahme von C oder C ++) nach Möglichkeit zu vermeiden, da dies die Dinge erheblich komplexiert. Wenn Sie Indizes verwenden, wird stattdessen alles einfacher, da Indizes nicht mit Kopien ungültig werden (entweder des gesamten Anwendungsstatus oder eines Teils davon). Wenn Sie beispielsweise bei einer Diagrammdatenstruktur diese vollständig kopieren oder nur Deltas aufzeichnen möchten, wenn Benutzer neue Verbindungen herstellen oder Verbindungen im Diagramm trennen, möchte der Status Verknüpfungen zu den alten Knoten speichern, auf die Sie stoßen können Probleme mit der Ungültigmachung und solchen Dingen. Es ist viel einfacher, wenn die Verbindungen relative Indizes in einem Array verwenden, da es viel einfacher ist, zu verhindern, dass diese Indizes ungültig werden, als Zeiger.


quelle
1
Alter Thread .. jeden Tag Problem auf einem Rückgängig-System;).
Gbianchi
0

Ich würde eine Rückgängig-Liste der (gültigen) Züge führen, die der Spieler gemacht hat.
Jedes Element in der Liste sollte genügend Informationen enthalten, um den Spielstatus auf den Stand vor dem Zug wiederherzustellen. Abhängig von der Anzahl der vorhandenen Spielzustände und der Anzahl der Rückgängig-Schritte, die Sie unterstützen möchten, kann dies eine Momentaufnahme des Spielzustands oder Informationen sein, die der Spiel-Engine mitteilen, wie der Zug rückgängig gemacht werden soll.

Bart van Ingen Schenau
quelle