Wie würde ich ein Wiedergabesystem entwerfen?
Sie kennen es möglicherweise aus bestimmten Spielen wie Warcraft 3 oder Starcraft, in denen Sie das Spiel erneut ansehen können, nachdem es bereits gespielt wurde.
Am Ende haben Sie eine relativ kleine Wiedergabedatei. Meine Fragen sind also:
- Wie speichere ich die Daten? (benutzerdefiniertes Format?) (kleine Dateigröße)
- Was soll gerettet werden?
- Wie kann man es generisch machen, damit es in anderen Spielen verwendet werden kann, um einen Zeitraum aufzuzeichnen (und zum Beispiel kein vollständiges Match)?
- Vor- und Zurückspulen möglich machen (WC3 konnte, soweit ich mich erinnere, nicht zurückspulen)
architecture
data-structure
file-format
game-recording
skalierbar
quelle
quelle
Antworten:
Dieser hervorragende Artikel behandelt viele Probleme: http://www.gamasutra.com/view/feature/2029/developing_your_own_replay_system.php
Ein paar Dinge, die der Artikel erwähnt und gut tut:
Eine Möglichkeit, das Komprimierungsverhältnis in den meisten Fällen weiter zu verbessern, besteht darin, alle Ihre Eingabestreams zu entkoppeln und sie unabhängig voneinander mit vollständiger Lauflängencodierung zu versehen. Dies ist ein Gewinn gegenüber der Delta-Codierungstechnik, wenn Sie Ihren Lauf in 8-Bit codieren und der Lauf selbst 8 Frames überschreitet (sehr wahrscheinlich, es sei denn, Ihr Spiel ist ein echter Button-Stampfer). Ich habe diese Technik in einem Rennspiel verwendet, um 8 Minuten Eingaben von 2 Spielern zu komprimieren, während ich auf einer Strecke bis auf ein paar hundert Bytes raste.
Um ein solches System wiederverwendbar zu machen, habe ich dafür gesorgt, dass das Wiedergabesystem generische Eingabestreams verarbeitet, aber auch Hooks bereitstellt, mit denen die spielspezifische Logik Tastatur-, Gamepad- und Mauseingaben in diese Streams marshallt.
Wenn Sie schnell zurückspulen oder nach dem Zufallsprinzip suchen möchten, können Sie alle N Bilder einen Prüfpunkt (Ihren vollständigen Gamestate) speichern. N sollte ausgewählt werden, um die Größe der Wiedergabedatei zu minimieren und um sicherzustellen, dass die Wartezeit des Players angemessen ist, während der Status bis zum ausgewählten Punkt wiedergegeben wird. Eine Möglichkeit, dies zu umgehen, besteht darin, sicherzustellen, dass zufällige Suchvorgänge nur an genau diesen Kontrollpunktpositionen durchgeführt werden können. Beim Zurückspulen wird der Spielstatus auf den Kontrollpunkt unmittelbar vor dem betreffenden Bild gesetzt und die Eingaben wiederholt, bis Sie zum aktuellen Bild gelangen. Wenn jedoch N zu groß ist, können Sie alle paar Frames ein Hitching bekommen. Eine Möglichkeit, diese Fehler zu glätten, besteht darin, die Frames zwischen den beiden vorherigen Checkpoints asynchron vorab zwischenzuspeichern, während Sie einen zwischengespeicherten Frame aus dem aktuellen Checkpoint-Bereich wiedergeben.
quelle
Neben der Lösung "Sicherstellen, dass die Tastenanschläge wiedergegeben werden können", die überraschend schwierig sein kann, können Sie einfach den gesamten Spielstatus auf jedem Frame aufzeichnen. Mit ein wenig cleverer Komprimierung können Sie es erheblich komprimieren. So handhabt Braid seinen Code zum Zurückspulen der Zeit und es funktioniert ziemlich gut.
Da Sie zum Zurückspulen ohnehin ein Checkpointing benötigen, können Sie versuchen, es auf einfache Weise zu implementieren, bevor Sie die Dinge komplizieren.
quelle
n
Sekunden von speichern das Spiel.Sie können Ihr System , als ob es zusammengesetzt war aus einer Reihe von Staaten und Funktionen, in denen eine Funktion anzuzeigen
f[j]
mit Eingabex[j]
ändert den Systemzustands[j]
in dem Zustands[j+1]
, etwa so:Ein Staat ist die Erklärung Ihrer gesamten Welt. Die Standorte des Spielers, der Standort des Feindes, die Punktzahl, die verbleibende Munition usw. Alles, was Sie benötigen, um einen Rahmen Ihres Spiels zu zeichnen.
Eine Funktion ist alles, was die Welt beeinflussen kann. Ein Frame-Wechsel, ein Tastendruck, ein Netzwerkpaket.
Die Eingabe sind die Daten, die die Funktion annimmt. Ein Frame-Wechsel kann die Zeit in Anspruch nehmen, die seit dem letzten Frame vergangen ist. Der Tastendruck kann die tatsächlich gedrückte Taste sowie die Frage umfassen, ob die Umschalttaste gedrückt wurde oder nicht.
Aus Gründen dieser Erklärung werde ich die folgenden Annahmen treffen:
Annahme 1:
Die Anzahl der Zustände für einen bestimmten Lauf des Spiels ist viel größer als die Anzahl der Funktionen. Sie haben wahrscheinlich Hunderttausende von Zuständen, aber nur einige Dutzend Funktionen (Rahmenwechsel, Tastendruck, Netzwerkpaket usw.). Natürlich muss die Anzahl der Eingaben gleich der Anzahl der Zustände minus eins sein.
Annahme 2:
Die räumlichen Kosten (Speicher, Platte) zum Speichern eines einzelnen Zustands sind viel größer als die zum Speichern einer Funktion und ihrer Eingabe.
Annahme 3:
Die zeitlichen Kosten (Zeit) für die Darstellung eines Zustands sind ähnlich oder nur ein oder zwei Größenordnungen länger als die für die Berechnung einer Funktion über einem Zustand.
Abhängig von den Anforderungen Ihres Wiedergabesystems gibt es verschiedene Möglichkeiten, ein Wiedergabesystem zu implementieren, sodass wir mit dem einfachsten beginnen können. Ich werde auch ein kleines Beispiel mit dem Schachspiel machen, das auf Zetteln aufgezeichnet ist.
Methode 1:
Speichern
s[0]...s[n]
. Das ist sehr einfach, sehr unkompliziert. Aufgrund der Annahme 2 sind die räumlichen Kosten dafür ziemlich hoch.Für Schach würde dies durch Ziehen des gesamten Brettes für jeden Zug erreicht.
Methode 2:
Wenn Sie nur eine Vorwärtswiedergabe benötigen, können Sie einfach speichern
s[0]
und dann speichernf[0]...f[n-1]
(denken Sie daran, dies ist nur der Name der ID der Funktion) undx[0]...x[n-1]
(was war die Eingabe für jede dieser Funktionen). Zum Wiederholen beginnen Sie einfach mits[0]
und berechnenund so weiter...
Ich möchte hier eine kleine Anmerkung machen. Mehrere andere Kommentatoren sagten, dass das Spiel "deterministisch sein muss". Jeder, der dies sagt, muss Computer Science 101 erneut absolvieren, denn ALLE COMPUTERPROGRAMME SIND DETERMINISTISCH¹, es sei denn, Ihr Spiel soll auf Quantencomputern ausgeführt werden. Das ist es, was Computer so großartig macht.
Da Ihr Programm jedoch höchstwahrscheinlich von externen Programmen abhängt, die von Bibliotheken bis zur tatsächlichen Implementierung der CPU reichen, kann es schwierig sein, sicherzustellen, dass sich Ihre Funktionen auf verschiedenen Plattformen gleich verhalten.
Wenn Sie Pseudozufallszahlen verwenden, können Sie entweder die generierten Zahlen als Teil Ihrer Eingabe
x
oder den Status der PRNG-Funktion als Teil Ihres Statuss
und die Implementierung als Teil der Funktion speichernf
.Für Schach würde dies erreicht werden, indem das ursprüngliche Brett (das bekannt ist) gezeichnet und dann jeder Zug beschrieben wird, wobei angegeben wird, welche Figur wohin gegangen ist. So machen sie es übrigens auch.
Methode 3:
Jetzt möchten Sie höchstwahrscheinlich in der Lage sein, in Ihre Wiederholung zu suchen. Das heißt, rechnen Sie
s[n]
für eine beliebigen
. Wenn Sie Methode 2 verwenden, müssen Sie erst rechnen,s[0]...s[n-1]
bevor Sie rechnen könnens[n]
, was nach Annahme 2 recht langsam sein kann.Um dies zu implementieren, ist Methode 3 eine Verallgemeinerung der Methoden 1 und 2: Speichern
f[0]...f[n-1]
undx[0]...x[n-1]
genau wie Methode 2, aber auch Speicherns[j]
für allej % Q == 0
für eine gegebene KonstanteQ
. Einfacher ausgedrückt bedeutet dies, dass Sie ein Lesezeichen in jedemQ
Bundesstaat speichern . Zum Beispiel fürQ == 100
Sie speicherns[0], s[100], s[200]...
Um
s[n]
für eine beliebige zu berechnenn
, laden Sie zuerst die zuvor gespeichertens[floor(n/Q)]
und berechnen dann alle Funktionen vonfloor(n/Q)
bisn
. Sie berechnen höchstensQ
Funktionen. Kleinere Werte vonQ
sind schneller zu berechnen, belegen jedoch viel mehr Speicherplatz, während größere Werte vonQ
weniger Speicherplatz belegen, die Berechnung jedoch länger dauert.Methode 3 mit
Q==1
ist dasselbe wie Methode 1, während Methode 3 mitQ==inf
dasselbe wie Methode 2 ist.Für Schach wird dies durch Ziehen jedes Zuges sowie eines von 10 Brettern (für
Q==10
) erreicht.Methode 4:
Wenn Sie umkehren Wiederholung möchten, können Sie eine kleine Variation des Verfahrens machen 3. Nehmen wir an
Q==100
, und Sie möchten berechnen ,s[150]
durchs[90]
in umgekehrter Richtung. Mit der unveränderten Methode 3 müssen Sie 50 Berechnungen durchführen, um zu erhalten,s[150]
und dann 49 weitere Berechnungen, um zu erhaltens[149]
und so weiter. Aber da Sie bereits berechnet habens[149]
, um zu erhaltens[150]
, können Sie einen Cache mit erstellen,s[100]...s[150]
wenn Sies[150]
zum ersten Mal berechnen , und Sie sind dann bereitss[149]
im Cache, wenn Sie ihn anzeigen müssen.Sie müssen nur den Cache jedes Mal , wenn Sie berechnen müssen regenerieren
s[j]
, fürj==(k*Q)-1
gegeben für jedenk
. Diesmal führt das ErhöhenQ
zu einer kleineren Größe (nur für den Cache), aber zu längeren Zeiten (nur zum Neuerstellen des Caches). Ein optimaler Wert fürQ
kann berechnet werden, wenn Sie die zur Berechnung von Zuständen und Funktionen erforderlichen Größen und Zeiten kennen.Für das Schachspiel würde dies durch das Zeichnen jeder Bewegung sowie einer von 10 Tafeln (für
Q==10
) erreicht, aber es würde auch erforderlich sein, die letzten 10 Tafeln, die Sie berechnet haben, in einem separaten Blatt Papier zu zeichnen.Methode 5:
Wenn Zustände einfach zu viel Platz verbrauchen oder Funktionen zu viel Zeit verbrauchen, können Sie eine Lösung erstellen, die die umgekehrte Wiedergabe tatsächlich implementiert (keine Fälschungen). Dazu müssen Sie für jede Ihrer Funktionen Reverse-Funktionen erstellen. Dies setzt jedoch voraus, dass jede Ihrer Funktionen eine Injektion ist. Wenn dies machbar ist, ist
f'
dasf
Berechnen zur Bezeichnung der Umkehrung der Funktions[j-1]
so einfach wieBeachten Sie, dass hier sind die Funktion und die Eingabe beide
j-1
nichtj
. Dieselbe Funktion und Eingabe wären die, die Sie verwendet hätten, wenn Sie gerechnet hättenDas Inverse dieser Funktionen zu erzeugen, ist der schwierige Teil. Dies ist jedoch normalerweise nicht möglich, da einige Statusdaten normalerweise nach jeder Funktion in einem Spiel verloren gehen.
Diese Methode kann, wie sie ist, die Berechnung rückgängig machen
s[j-1]
, jedoch nur, wenn Sie dies getan habens[j]
. Dies bedeutet, dass Sie die Wiedergabe nur rückwärts sehen können, beginnend an dem Punkt, an dem Sie beschlossen haben, sie rückwärts abzuspielen. Wenn Sie von einem beliebigen Punkt aus rückwärts abspielen möchten, müssen Sie dies mit Methode 4 mischen.Für Schach kann dies nicht implementiert werden, da Sie mit einem bestimmten Brett und dem vorherigen Zug wissen können, welche Figur bewegt wurde, aber nicht, woher sie gezogen wurde.
Methode 6:
Wenn Sie nicht garantieren können, dass alle Ihre Funktionen Injektionen sind, können Sie einen kleinen Trick machen, um dies zu tun. Anstatt zu veranlassen, dass jede Funktion nur einen neuen Status zurückgibt, können Sie auch die verworfenen Daten zurückgeben:
Wo
r[j]
sind die verworfenen Daten. Und dann erstellen Sie Ihre inversen Funktionen so, dass sie die verworfenen Daten wie folgt aufnehmen:Zusätzlich zu
f[j]
undx[j]
müssen Sie auchr[j]
für jede Funktion speichern . Wenn Sie erneut suchen möchten, müssen Sie Lesezeichen speichern, z. B. mit Methode 4.Für Schach wäre dies dasselbe wie für Methode 2, aber im Gegensatz zu Methode 2, die nur sagt, welche Figur wohin geht, müssen Sie auch speichern, woher jede Figur stammt.
Implementierung:
Da dies für alle Arten von Zuständen mit allen Arten von Funktionen für ein bestimmtes Spiel funktioniert, können Sie verschiedene Annahmen treffen, die die Implementierung erleichtern. Wenn Sie Methode 6 mit dem gesamten Spielstatus implementieren, können Sie nicht nur die Daten wiedergeben, sondern auch die Zeit zurückverfolgen und das Spiel zu einem bestimmten Zeitpunkt fortsetzen. Das wäre ziemlich genial.
Anstatt den gesamten Spielstatus zu speichern, können Sie einfach das Nötigste speichern, das Sie zum Zeichnen eines bestimmten Status benötigen, und diese Daten für einen festgelegten Zeitraum serialisieren. Ihre Zustände sind diese Serialisierungen, und Ihre Eingabe ist jetzt der Unterschied zwischen zwei Serialisierungen. Der Schlüssel dafür ist, dass sich die Serialisierung nur geringfügig ändern sollte, wenn sich auch der Weltstaat nur geringfügig ändert. Dieser Unterschied ist vollständig umkehrbar, so dass die Implementierung von Methode 5 mit Lesezeichen sehr gut möglich ist.
Ich habe dies in einigen großen Spielen implementiert gesehen, hauptsächlich für die sofortige Wiedergabe der neuesten Daten, wenn ein Ereignis eintritt (ein Teil in fps oder eine Punktzahl in Sportspielen).
Ich hoffe, diese Erklärung war nicht zu langweilig.
¹ Dies bedeutet nicht, dass einige Programme nicht deterministisch sind (z. B. MS Windows ^^). Im Ernst, wenn Sie ein nicht deterministisches Programm auf einem deterministischen Computer erstellen können, können Sie ziemlich sicher sein, dass Sie gleichzeitig die Fields-Medaille, den Turing-Preis und wahrscheinlich sogar einen Oscar und einen Grammy für alles gewinnen, was es wert ist.
quelle
Eine Sache, die andere Antworten noch nicht behandelt haben, ist die Gefahr von Schwimmern. Sie können mit Floats keine vollständig deterministische Anwendung erstellen.
Mit floats können Sie ein vollständig deterministisches System haben, aber nur wenn:
Dies liegt daran, dass die interne Darstellung von Floats von einer CPU zur anderen variiert - am dramatischsten zwischen AMD- und Intel-CPUs. Solange sich die Werte in den FPU-Registern befinden, sind sie genauer als auf der C-Seite. Daher werden alle Zwischenberechnungen mit höherer Präzision durchgeführt.
Es ist ziemlich offensichtlich, wie sich dies auf das AMD-Bit im Vergleich zum Intel-Bit auswirkt. Nehmen wir beispielsweise an, dass das eine 80-Bit-Float und das andere 64-Bit-Float verwendet. Warum aber die gleiche Binäranforderung?
Wie gesagt, die höhere Genauigkeit wird verwendet , solange sich die Werte in den FPU-Registern befinden . Dies bedeutet, dass Ihre Compiler-Optimierung bei jeder Neukompilierung möglicherweise Werte in die FPU-Register und aus diesen austauscht, was zu geringfügig unterschiedlichen Ergebnissen führt.
Möglicherweise können Sie hier Abhilfe schaffen, indem Sie die Flags _control87 () / _ controlfp () auf die niedrigstmögliche Genauigkeit setzen. Einige Bibliotheken berühren dies jedoch möglicherweise auch (zumindest einige Versionen von d3d).
quelle
Speichern Sie den Ausgangszustand Ihrer Zufallsgeneratoren. Speichern Sie dann jede Eingabe mit einem Zeitstempel (Maus, Tastatur, Netzwerk, was auch immer). Wenn Sie ein vernetztes Spiel haben, haben Sie dieses wahrscheinlich bereits.
Stellen Sie die RNGs neu ein und geben Sie den Eingang wieder. Das ist es.
Dies löst nicht das Zurückspulen, für das es keine allgemeine Lösung gibt, sondern das Wiedergeben von Anfang an, so schnell Sie können. Sie können die Leistung dafür verbessern, indem Sie den gesamten Spielstatus alle X Sekunden überprüfen. Dann müssen Sie immer nur so viele wiederholen, aber der gesamte Spielstatus ist möglicherweise auch unerschwinglich teuer.
Die Details des Dateiformats spielen keine Rolle, aber die meisten Engines haben die Möglichkeit, Befehle und Status bereits zu serialisieren - zum Vernetzen, Speichern oder was auch immer. Verwenden Sie das einfach.
quelle
Ich würde gegen eine deterministische Wiedergabe stimmen. Es ist weitaus einfacher und weitaus weniger fehleranfällig, den Zustand jeder Entität jede 1 / N-Sekunde zu speichern.
Speichern Sie genau das, was Sie bei der Wiedergabe anzeigen möchten - wenn es nur um Position und Überschrift geht, gut, wenn Sie auch Statistiken anzeigen möchten, speichern Sie dies ebenfalls, aber im Allgemeinen so wenig wie möglich.
Ändern Sie die Codierung. Verwenden Sie für alles so wenig Bits wie möglich. Die Wiedergabe muss nicht perfekt sein, solange sie gut genug aussieht. Selbst wenn Sie beispielsweise einen Gleitkommawert für die Überschrift verwenden, können Sie ihn in einem Byte speichern und 256 mögliche Werte (Genauigkeit 1,4º) abrufen. Das kann genug oder sogar zu viel für Ihr spezielles Problem sein.
Verwenden Sie die Delta-Codierung. Wenn sich Ihre Einheiten nicht teleportieren (und den Fall dann separat behandeln), kodieren Sie Positionen als Differenz zwischen der neuen Position und der alten Position. Bei kurzen Bewegungen können Sie mit weitaus weniger Bits davonkommen, als Sie für volle Positionen benötigen würden .
Wenn Sie einen einfachen Rücklauf wünschen, fügen Sie alle N Bilder Keyframes (vollständige Daten, keine Deltas) hinzu. Auf diese Weise können Sie mit einer geringeren Genauigkeit für Deltas und andere Werte davonkommen. Rundungsfehler sind nicht so problematisch, wenn Sie regelmäßig auf "wahre" Werte zurücksetzen.
Zum Schluss gzip das Ganze :)
quelle
Es ist schwer. Lesen Sie zuerst und vor allem die Antworten von Jari Komppa.
Eine auf meinem Computer vorgenommene Wiedergabe funktioniert möglicherweise nicht auf Ihrem Computer, da das Floating-Ergebnis geringfügig anders ist. Es ist ein großes Geschäft.
Aber wenn Sie danach Zufallszahlen haben, müssen Sie den Startwert in der Wiedergabe speichern. Laden Sie dann alle Standardzustände und setzen Sie die Zufallszahl auf diesen Startwert. Von dort aus können Sie einfach den aktuellen Tastatur- / Mausstatus und die Zeitspanne, in der dies der Fall war, aufzeichnen. Führen Sie dann alle Ereignisse aus, die diese als Eingabe verwenden.
Um in Dateien zu springen (was sehr viel schwieriger ist), müssen Sie THE MEMORY sichern. Wie, wo jede Einheit ist, Geld, Zeit vergeht, den gesamten Spielstatus. Dann schnell vorwärts spulen, aber alles wiedergeben, außer Rendering, Sound usw. überspringen, bis Sie das gewünschte Zeitziel erreicht haben. Dies kann jede Minute oder 5 Minuten geschehen, je nachdem, wie schnell die Weiterleitung erfolgt.
Die wichtigsten Punkte sind - Umgang mit Zufallszahlen - Kopieren von Eingaben (Player (s) und Remote-Player (s)) - Speicherauszug für das Herumspringen von Dateien und ... - FLOAT NOT BREAK THINGS (ja, ich musste schreien)
quelle
Ich bin etwas überrascht, dass niemand diese Option erwähnt hat, aber wenn Ihr Spiel eine Multiplayer-Komponente hat, haben Sie möglicherweise bereits viel harte Arbeit für diese Funktion geleistet. Was ist eigentlich Multiplayer als der Versuch, die Bewegungen einer anderen Person zu einem (etwas) anderen Zeitpunkt auf Ihrem eigenen Computer wiederzugeben?
Dies bringt Ihnen auch die Vorteile einer kleineren Dateigröße als Nebeneffekt, vorausgesetzt, Sie haben bereits an bandbreitenschonendem Netzwerkcode gearbeitet.
In vielerlei Hinsicht kombiniert es sowohl die Optionen "extrem deterministisch sein" als auch "alles festhalten". Sie werden immer noch Determinismus brauchen - wenn Ihre Wiederholung im Wesentlichen darin besteht, dass Bots das Spiel genau so wiedergeben, wie Sie es ursprünglich gespielt haben, müssen die Aktionen, die sie ausführen, zufällige Ergebnisse haben, dasselbe Ergebnis haben.
Das Datenformat könnte so einfach wie ein Speicherauszug des Netzwerkverkehrs sein, obwohl ich mir vorstellen würde, dass es nicht schaden würde, ihn ein wenig zu bereinigen (Sie müssen sich schließlich keine Gedanken über Verzögerungen bei der erneuten Wiedergabe machen). Sie können nur einen Teil des Spiels erneut spielen, indem Sie den von anderen erwähnten Checkpoint-Mechanismus verwenden. In der Regel sendet ein Multiplayer-Spiel ohnehin immer wieder einen vollständigen Status des Spiel-Updates, sodass Sie diese Arbeit möglicherweise bereits ausgeführt haben.
quelle
Um die kleinstmögliche Wiedergabedatei zu erhalten, müssen Sie sicherstellen, dass Ihr Spiel deterministisch ist. In der Regel müssen Sie Ihren Zufallszahlengenerator anschauen und feststellen, wo er in der Spielelogik verwendet wird.
Sie werden höchstwahrscheinlich ein RNG für die Spielelogik und ein RNG für alles andere für Dinge wie GUI, Partikeleffekte und Sounds benötigen. Sobald Sie dies getan haben, müssen Sie den Anfangszustand der Spiellogik RNG aufzeichnen, dann die Spielbefehle aller Spieler in jedem Frame.
Bei vielen Spielen gibt es eine Abstraktionsebene zwischen der Eingabe und der Spielelogik, in der die Eingabe in Befehle umgewandelt wird. Zum Beispiel führt das Drücken der A-Taste auf dem Controller dazu, dass ein digitaler "Sprung" -Befehl auf wahr gesetzt wird und die Spiellogik auf Befehle reagiert, ohne den Controller direkt zu überprüfen. Auf diese Weise müssen Sie nur die Befehle aufzeichnen, die sich auf die Spiellogik auswirken (es ist nicht erforderlich, den Befehl "Pause" aufzuzeichnen), und diese Daten sind höchstwahrscheinlich kleiner als die Controller-Daten. Sie müssen sich auch nicht darum kümmern, den Status des Steuerungsschemas aufzuzeichnen, falls der Player die Tasten neu zuordnen möchte.
Das Zurückspulen ist ein schwieriges Problem, wenn Sie die deterministische Methode verwenden und nicht den Schnappschuss des Spielzustands und das schnelle Vorwärtsspulen zu dem Zeitpunkt, zu dem Sie sich ansehen möchten. Sie können nur den gesamten Spielzustand jedes Frames aufzeichnen.
Auf der anderen Seite ist ein schneller Vorlauf durchaus machbar. Solange Ihre Spielelogik nicht von Ihrem Rendering abhängt, können Sie die Spielelogik so oft ausführen, wie Sie möchten, bevor Sie einen neuen Frame des Spiels rendern. Die Geschwindigkeit des Schnellvorlaufs hängt nur von Ihrem Gerät ab. Wenn Sie in großen Schritten vorwärts springen möchten, müssen Sie dieselbe Snapshot-Methode verwenden, die Sie zum Zurückspulen benötigen würden.
Möglicherweise besteht der wichtigste Teil beim Schreiben eines Wiedergabesystems, das auf Determinismus beruht, darin, einen Debug-Datenstrom aufzuzeichnen. Dieser Debug-Stream enthält eine Momentaufnahme von so vielen Informationen wie möglich in jedem Frame (RNG-Seeds, Entitätstransformationen, Animationen usw.) und kann diesen aufgezeichneten Debug-Stream während der Wiederholungen gegen den Status des Spiels testen. Auf diese Weise können Sie Fehlpaarungen am Ende eines bestimmten Frames schnell erkennen. Dies erspart Ihnen unzählige Stunden, um unbekannten, nicht deterministischen Fehlern die Haare zu entreißen. Etwas so Einfaches wie eine nicht initialisierte Variable wird in der 11. Stunde alles durcheinander bringen.
HINWEIS: Wenn Ihr Spiel dynamisches Streaming von Inhalten beinhaltet oder Sie Spielelogik auf mehreren Threads oder auf verschiedenen Kernen haben ... viel Glück.
quelle
Um sowohl das Aufzeichnen als auch das Zurückspulen zu aktivieren, zeichnen Sie alle Ereignisse auf (vom Benutzer generiert, vom Timer generiert, von der Kommunikation generiert, ...).
Für jede Ereignisaufzeichnungszeit des Ereignisses, was geändert wurde, vorherige Werte, neue Werte.
Berechnete Werte müssen nicht aufgezeichnet werden, es sei denn, die Berechnung erfolgt nach dem Zufallsprinzip
(In diesen Fällen können Sie entweder auch berechnete Werte aufzeichnen oder Änderungen am Startwert nach jeder Zufallsberechnung aufzeichnen).
Die gespeicherten Daten sind eine Liste von Änderungen.
Änderungen können in verschiedenen Formaten (binär, xml, ...) gespeichert werden.
Die Änderung besteht aus Entitäts-ID, Eigenschaftsname, altem Wert und neuem Wert.
Stellen Sie sicher, dass Ihr System diese Änderungen wiedergeben kann (Zugriff auf die gewünschte Entität, Änderung der gewünschten Eigenschaft vorwärts in den neuen Zustand oder rückwärts in den alten Zustand).
Beispiel:
Um ein schnelleres Zurückspulen / Vorspulen zu ermöglichen oder nur bestimmte Zeitbereiche aufzuzeichnen, sind
Schlüsselbilder erforderlich - wenn Sie ständig aufzeichnen, speichern Sie ab und zu den gesamten Spielstatus.
Wenn Sie nur einen bestimmten Zeitraum aufzeichnen, speichern Sie zu Beginn den Ausgangszustand.
quelle
Wenn Sie Ideen zur Implementierung Ihres Wiedergabesystems benötigen, suchen Sie in Google nach Möglichkeiten zum Rückgängigmachen / Wiederherstellen in einer Anwendung. Es mag für einige, aber nicht für alle offensichtlich sein, dass das Rückgängigmachen / Wiederherstellen konzeptionell mit dem Wiederholen von Spielen identisch ist. Es ist nur ein Sonderfall, in dem Sie zurückspulen und je nach Anwendung zu einem bestimmten Zeitpunkt suchen können.
Sie werden feststellen, dass sich niemand, der Undo / Redo implementiert, über deterministische / nicht deterministische Variablen, Float-Variablen oder bestimmte CPUs beschwert.
quelle