So entwerfen Sie ein Wiedergabesystem


75

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)

3
Obwohl die folgenden Antworten viele wertvolle Erkenntnisse liefern, möchte ich nur betonen, wie wichtig es ist, Ihr Spiel / Ihre Engine so zu entwickeln, dass sie sehr deterministisch ist ( en.wikipedia.org/wiki/Deterministic_algorithm ), da dies für die Erreichung Ihres Ziels unerlässlich ist.
Ari Patrick

2
Beachten Sie auch, dass Physik-Engines nicht deterministisch sind (Havok behauptet, dass dies der Fall ist ...), sodass die Lösung, nur die Eingaben und Zeitstempel zu speichern, jedes Mal unterschiedliche Ergebnisse liefert, wenn Ihr Spiel Physik verwendet.
Samaursa

5
Die meisten Physik-Engines sind deterministisch, solange Sie einen festen Zeitschritt verwenden, den Sie sowieso tun sollten. Ich wäre sehr überrascht, wenn Havok nicht ist. Nicht-Determinismus ist auf Computern ziemlich schwer zu bekommen ...

4
Deterministisch bedeutet gleiche Eingaben = gleiche Ausgaben. Wenn Sie Floats auf einer Plattform haben und auf einer anderen verdoppeln (zum Beispiel) oder Ihre IEEE-Fließkomma-Standardimplementierung absichtlich deaktiviert haben, bedeutet dies, dass Sie nicht mit denselben Eingaben arbeiten, und nicht, dass dies nicht deterministisch ist.

3
Bin ich es oder bekommt diese Frage jede zweite Woche ein Kopfgeld?
Die kommunistische Ente

Antworten:


39

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:

  • Ihr Spiel muss deterministisch sein.
  • Es zeichnet den Anfangszustand des Spielsystems im ersten Frame und nur die Eingaben des Spielers während des Spiels auf.
  • Quantisierung der Eingänge, um die Anzahl der Bits zu verringern. Dh stellen Schwebungen in verschiedenen Bereichen dar (z. B. [0, 1] oder [-1, 1] innerhalb weniger Bits). Quantisierte Eingaben müssen auch während des tatsächlichen Spiels erhalten werden.
  • Verwenden Sie ein einzelnes Bit, um zu bestimmen, ob ein Eingabestream neue Daten enthält. Da sich einige Streams nicht häufig ändern, wird die zeitliche Kohärenz der Eingaben ausgenutzt.

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.


Wenn es sich um RNG handelt, nehmen Sie die Ergebnisse dieser RNG in die Streams auf
Ratschenfreak

1
@ratchet Freak: Bei deterministischer Verwendung von PRNG kommt man damit zurecht, dass man während der Checkpoints nur seinen Samen speichert.
NonNumeric

22

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.


2
+1 Mit einer cleveren Komprimierung können Sie die Menge der Daten, die Sie speichern müssen, wirklich reduzieren (speichern Sie beispielsweise den Status nicht, wenn er sich nicht im Vergleich zum letzten Status geändert hat, den Sie für das aktuelle Objekt gespeichert haben). . Ich habe das schon mit Physik versucht und es funktioniert wirklich gut. Wenn Sie nicht über Physik verfügen und nicht das gesamte Spiel zurückspulen möchten, würde ich mich für Joes Lösung entscheiden, da diese nur die kleinstmöglichen Dateien erzeugt. Wenn Sie auch zurückspulen möchten, können Sie nur die letzten nSekunden von speichern das Spiel.
Samaursa

@Samaursa - Wenn Sie Standard-Komprimierungsbibliotheken (z. B. gzip) verwenden, erhalten Sie die gleiche (wahrscheinlich bessere) Komprimierung, ohne manuell prüfen zu müssen, ob sich der Status geändert hat oder nicht.
Justin

2
@Kragen: Nicht wirklich wahr. Standardkomprimierungsbibliotheken sind sicherlich gut, können jedoch häufig nicht auf domänenspezifisches Wissen zurückgreifen. Wenn Sie ihnen ein wenig helfen können, indem Sie ähnliche Daten nebeneinander platzieren und Dinge entfernen, die sich wirklich nicht geändert haben, können Sie die Dinge erheblich reduzieren.
ZorbaTHut

1
@ZorbaTHut Theoretisch ja, aber in der Praxis lohnt sich der Aufwand wirklich?
Justin

4
Ob sich der Aufwand lohnt, hängt ganz davon ab, über wie viele Daten Sie verfügen. Wenn Sie ein RTS mit Hunderten oder Tausenden von Einheiten haben, ist dies wahrscheinlich von Bedeutung. Wenn Sie die Wiederholungen im Speicher wie Braid speichern müssen, ist dies wahrscheinlich von Bedeutung.

21

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 Eingabe x[j]ändert den Systemzustand s[j]in dem Zustand s[j+1], etwa so:

s[j+1] = f[j](s[j], x[j])

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 speichern f[0]...f[n-1](denken Sie daran, dies ist nur der Name der ID der Funktion) und x[0]...x[n-1](was war die Eingabe für jede dieser Funktionen). Zum Wiederholen beginnen Sie einfach mit s[0]und berechnen

s[1] = f[0](s[0], x[0])
s[2] = f[1](s[1], x[1])

und 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 xoder den Status der PRNG-Funktion als Teil Ihres Status sund die Implementierung als Teil der Funktion speichern f.

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 beliebige n. Wenn Sie Methode 2 verwenden, müssen Sie erst rechnen, s[0]...s[n-1]bevor Sie rechnen können s[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]und x[0]...x[n-1]genau wie Methode 2, aber auch Speichern s[j]für alle j % Q == 0für eine gegebene Konstante Q. Einfacher ausgedrückt bedeutet dies, dass Sie ein Lesezeichen in jedem QBundesstaat speichern . Zum Beispiel für Q == 100Sie speicherns[0], s[100], s[200]...

Um s[n]für eine beliebige zu berechnen n, laden Sie zuerst die zuvor gespeicherten s[floor(n/Q)]und berechnen dann alle Funktionen von floor(n/Q)bis n. Sie berechnen höchstens QFunktionen. Kleinere Werte von Qsind schneller zu berechnen, belegen jedoch viel mehr Speicherplatz, während größere Werte von Qweniger Speicherplatz belegen, die Berechnung jedoch länger dauert.

Methode 3 mit Q==1ist dasselbe wie Methode 1, während Methode 3 mit Q==infdasselbe 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]durch s[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 erhalten s[149]und so weiter. Aber da Sie bereits berechnet haben s[149], um zu erhalten s[150], können Sie einen Cache mit erstellen, s[100]...s[150]wenn Sie s[150]zum ersten Mal berechnen , und Sie sind dann bereits s[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ür j==(k*Q)-1gegeben für jeden k. Diesmal führt das Erhöhen Qzu einer kleineren Größe (nur für den Cache), aber zu längeren Zeiten (nur zum Neuerstellen des Caches). Ein optimaler Wert für Qkann 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'das fBerechnen zur Bezeichnung der Umkehrung der Funktion s[j-1]so einfach wie

s[j-1] = f'[j-1](s[j], x[j-1])

Beachten Sie, dass hier sind die Funktion und die Eingabe beide j-1nicht j. Dieselbe Funktion und Eingabe wären die, die Sie verwendet hätten, wenn Sie gerechnet hätten

s[j] = f[j-1](s[j-1], x[j-1])

Das 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 haben s[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:

s[j+1], r[j] = f[j](s[j], x[j])

Wo r[j]sind die verworfenen Daten. Und dann erstellen Sie Ihre inversen Funktionen so, dass sie die verworfenen Daten wie folgt aufnehmen:

s[j] = f'[j](s[j+1], x[j], r[j])

Zusätzlich zu f[j]und x[j]müssen Sie auch r[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.


Bei "ALLE COMPUTERPROGRAMME SIND DETERMINISTISCH" vernachlässigen Sie die Berücksichtigung von Programmen, die auf Threading basieren. Während Threading hauptsächlich zum Laden von Ressourcen oder zum Trennen der Render-Schleife verwendet wird, gibt es Ausnahmen, und an diesem Punkt können Sie möglicherweise keinen echten Determinismus mehr behaupten, es sei denn, Sie sind streng genug, um den Determinismus durchzusetzen. Verriegelungsmechanismen allein reichen nicht aus. Ohne zusätzlichen Arbeitsaufwand könnten Sie KEINE veränderlichen Daten teilen. In vielen Szenarien benötigt ein Spiel diese Strenge nicht für sich selbst, sondern für Dinge wie Wiederholungen.
krdluzni

1
@krdluzni Threading, Parallelität und Zufallszahlen aus echten Zufallsquellen machen Programme nicht nicht deterministisch. Thread-Timings, Deadlocks, nicht initialisierter Speicher und sogar Race-Bedingungen sind nur zusätzliche Eingaben, die Ihr Programm benötigt. Ihre Wahl, diese Eingaben zu verwerfen oder gar nicht zu berücksichtigen (aus welchem ​​Grund auch immer), hat keinen Einfluss darauf, dass Ihr Programm bei genau denselben Eingaben genau dasselbe ausführt. "nicht deterministisch" ist ein sehr präziser Begriff aus der Informatik. Vermeiden Sie daher die Verwendung, wenn Sie nicht wissen, was dies bedeutet.

@oscar (Kann etwas knapp sein, beschäftigt, könnte später bearbeitet werden): Obwohl Sie in einem gewissen strengen theoretischen Sinne Thread-Timings usw. als Eingaben beanspruchen könnten, ist dies in praktischer Hinsicht nicht sinnvoll, da sie im Allgemeinen von der nicht beachtet werden können Programm selbst oder vollständig vom Entwickler gesteuert. Ferner unterscheidet sich ein Programm, das nicht deterministisch ist, signifikant davon, dass es nicht deterministisch ist (im Sinne einer Zustandsmaschine). Ich verstehe die Bedeutung des Begriffs. Ich wünschte, sie hätten sich für etwas anderes entschieden, anstatt einen bestehenden Begriff zu überfrachten.
krdluzni

@krdluzni Mein Punkt beim Entwerfen von Wiedergabesystemen mit unvorhersehbaren Elementen wie z. B. Thread-Timings (wenn sie sich auf Ihre Fähigkeit auswirken, eine Wiedergabe genau zu berechnen) ist, sie wie jede andere Eingabequelle zu behandeln, genau wie Benutzereingaben. Ich sehe niemanden, der sich über ein Programm beschwert, das "nicht deterministisch" ist, weil es völlig unvorhersehbare Benutzereingaben erfordert. Der Begriff ist ungenau und verwirrend. Ich möchte lieber, dass sie so etwas wie "praktisch unvorhersehbar" oder so etwas verwenden. Und nein, es ist nicht unmöglich, das Debuggen der Wiederholung von VMWare zu überprüfen.

9

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:

  • Verwenden Sie genau die gleiche Binärdatei
  • Mit genau der gleichen CPU

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).


3
Mit GCC können Sie -ffloat-store verwenden, um die Werte aus den Registern zu entfernen und auf eine Genauigkeit von 32/64 Bit zu kürzen, ohne sich um andere Bibliotheken kümmern zu müssen, die mit Ihren Steuerflags herumspielen. Dies wirkt sich natürlich negativ auf Ihre Geschwindigkeit aus (aber auch auf jede andere Quantisierung).

8

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.


4

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 :)


1
Dies hängt allerdings ein wenig vom Spieltyp ab.
Jari Komppa

Ich würde mit dieser Aussage sehr vorsichtig sein. Insbesondere bei größeren Projekten mit Abhängigkeiten von Drittanbietern kann das Speichern des Status unmöglich sein. Während des Zurücksetzens und der Wiedergabe ist die Eingabe immer möglich.
TomSmartBishop

2

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)


2

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.


0

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.


0

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:

  • Zeit von Start = t1, Entität = Spieler 1, Eigenschaft = Position, geändert von a nach b
  • Zeit von Start = t1, Entität = System, Eigenschaft = Spielmodus, geändert von c nach d
  • Zeit von Start = t2, Entität = Spieler 2, Eigenschaft = Zustand, geändert von e nach f
  • 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.


    -1

    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.


    Rückgängig / Wiederherstellen tritt in Anwendungen auf, die selbst von Grund auf deterministisch, ereignisgesteuert und zustandsabhängig sind (z. B. besteht der Zustand eines Textverarbeitungsdokuments nur aus Text und Auswahl, nicht aus dem gesamten Layout, das neu berechnet werden kann).

    Dann ist es offensichtlich, dass Sie noch nie CAD / CAM-Anwendungen, Schaltungsentwicklungssoftware, Bewegungsverfolgungssoftware oder eine Anwendung mit Rückgängig / Wiederherstellen verwendet haben, die ausgefeilter ist als ein Textverarbeitungsprogramm. Ich sage nicht, dass der Code zum Rückgängigmachen / Wiederherstellen für die Wiedergabe in einem Spiel kopiert werden kann, nur dass er konzeptionell derselbe ist (Status speichern und später wiedergeben). Die Hauptdatenstruktur ist jedoch keine Warteschlange, sondern ein Stapel.
    Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
    Licensed under cc by-sa 3.0 with attribution required.