Zum deterministischen Umgang mit Gleitkommazahlen
Fließkomma ist deterministisch. Nun, das sollte es sein. Es ist kompliziert.
Es gibt reichlich Literatur zu Fließkommazahlen:
Und wie problematisch sie sind:
Für abstrakte. Zumindest in einem einzelnen Thread sollten dieselben Vorgänge mit denselben Daten in derselben Reihenfolge deterministisch sein. Wir können also damit beginnen, uns Gedanken über Eingaben zu machen und die Reihenfolge zu ändern.
Eine solche Eingabe, die Probleme verursacht, ist die Zeit.
Zunächst sollten Sie immer den gleichen Zeitschritt berechnen. Ich sage nicht, um die Zeit nicht zu messen, ich sage, dass Sie keine Zeit an die Physiksimulation weitergeben werden, da zeitliche Schwankungen eine Rauschquelle in der Simulation sind.
Warum messen Sie die Zeit, wenn Sie sie nicht an die Physiksimulation weitergeben? Sie möchten die verstrichene Zeit messen, um zu wissen, wann ein Simulationsschritt aufgerufen werden soll, und - vorausgesetzt, Sie verwenden Schlaf - wie viel Zeit zum Schlafen.
Somit:
- Zeit messen: Ja
- Zeit in der Simulation verwenden: Nein
Nun, Anweisung neu anordnen.
Der Compiler könnte entscheiden, dass dies f * a + b
dasselbe ist b + f * a
, jedoch kann dies zu einem anderen Ergebnis führen. Es könnte auch kompiliert werden, um fmadd zu erstellen , oder es könnte entscheiden, dass mehrere Zeilen, die zusammen auftreten, mit SIMD oder einer anderen Optimierung, die mir derzeit nicht in den Sinn kommt , geschrieben werden . Und denken Sie daran, wir möchten, dass die gleichen Vorgänge in der gleichen Reihenfolge ausgeführt werden. Es liegt nahe, dass wir steuern möchten, welche Vorgänge ausgeführt werden.
Und nein, mit double werden Sie nicht gerettet.
Sie müssen sich Gedanken über den Compiler und seine Konfiguration machen, insbesondere um Gleitkommazahlen im Netzwerk zu synchronisieren. Sie müssen die Builds dazu bringen, zuzustimmen, dasselbe zu tun.
Eine Schreibanordnung wäre wohl ideal. Auf diese Weise entscheiden Sie, welche Operation durchgeführt werden soll. Dies könnte jedoch ein Problem für die Unterstützung mehrerer Plattformen sein.
Somit:
Der Fall für Festkommazahlen
Aufgrund der Art und Weise, wie Floats im Speicher dargestellt werden, werden große Werte an Genauigkeit verlieren. Es liegt nahe, Ihre Werte klein zu halten (Clamp), um das Problem zu mindern. Somit keine großen Geschwindigkeiten und keine großen Räume. Dies bedeutet auch, dass Sie diskrete Physik verwenden können, da Sie ein geringeres Tunnelungsrisiko haben.
Andererseits häufen sich kleine Fehler an. Also abschneiden. Ich meine, skalieren und auf einen ganzzahligen Typ umwandeln. Auf diese Weise wissen Sie, dass sich nichts aufbaut. Es wird Operationen geben, die Sie mit dem Integer-Typ durchführen können. Wenn Sie zum Gleitkomma zurückkehren müssen, können Sie die Skalierung umwandeln und rückgängig machen.
Beachten Sie, dass ich Skala sage. Die Idee ist, dass 1 Einheit tatsächlich als Zweierpotenz dargestellt wird (zum Beispiel 16384). Was auch immer es ist, mach es zu einer Konstanten und benutze es. Sie verwenden es grundsätzlich als Festkommazahl. In der Tat, wenn Sie richtige Festkommazahlen aus einer zuverlässigen Bibliothek viel besser verwenden können.
Ich sage abgeschnitten. In Bezug auf das Rundungsproblem bedeutet dies, dass Sie dem letzten Teil des Wertes, den Sie nach der Besetzung erhalten haben, nicht vertrauen können. Also, bevor die Besetzungsskala ein bisschen mehr als Sie brauchen, und kürzen Sie es danach.
Somit:
- Werte klein halten: Ja
- Sorgfältige Rundung: Ja
- Festkommazahlen wenn möglich: Ja
Warten Sie, warum brauchen Sie Gleitkomma? Könnten Sie nicht nur mit einem Integer-Typ arbeiten? Oh, richtig. Trigonometrie und Bestrahlung. Sie können Tabellen für Trigonometrie und Radikation berechnen und in Ihrer Quelle backen lassen. Sie können auch die Algorithmen implementieren, mit denen sie mit Gleitkommazahlen berechnet werden, außer mit Festkommazahlen. Ja, Sie müssen Speicher, Leistung und Präzision in Einklang bringen. Sie könnten sich jedoch von Gleitkommazahlen fernhalten und deterministisch bleiben.
Wusstest du, dass sie so etwas für die ursprüngliche PlayStation gemacht haben? Bitte treffen Sie meinen Hund, Flecken .
Übrigens sage ich nicht, Fließkomma für Grafiken nicht zu verwenden. Nur für die Physik. Ich meine, klar, die Positionen werden von der Physik abhängen. Wie Sie wissen, muss ein Collider jedoch nicht mit einem Modell übereinstimmen. Wir wollen die Ergebnisse der Kürzung der Modelle nicht sehen.
Also: FESTE PUNKTNUMMERN VERWENDEN.
Um es klar auszudrücken: Wenn Sie einen Compiler verwenden können, mit dem Sie die Funktionsweise von Fließkommazahlen festlegen können und der für Sie ausreicht, können Sie dies tun. Das ist nicht immer eine Option. Außerdem tun wir dies für den Determinismus. Festkommazahlen bedeuten nicht, dass es keine Fehler gibt, schließlich haben sie eine begrenzte Genauigkeit.
Ich denke nicht, dass "Festkommazahlen hart sind" ein guter Grund ist, sie nicht zu verwenden. Und wenn Sie einen guten Grund haben möchten, sie zu verwenden, dann ist es Determinismus, insbesondere Determinismus über Plattformen hinweg.
Siehe auch:
Nachtrag : Ich schlage vor, die Größe der Welt klein zu halten. Trotzdem bringen sowohl OP als auch Jibb Smart den Punkt auf den Punkt, dass die Bewegung von den Ursprungs-Floats weniger genau ist. Das wird Auswirkungen auf die Physik haben, die weit früher als der Rand der Welt zu sehen sein wird. Festkommazahlen haben eine feste Genauigkeit und sind überall gleich gut (oder schlecht, wenn Sie es vorziehen). Was gut ist, wenn wir Determinismus wollen. Ich möchte auch erwähnen, dass die Art und Weise, wie wir normalerweise Physik betreiben, die Eigenschaft hat, kleine Variationen zu verstärken. Siehe Der Schmetterlingseffekt - Deterministische Physik in The Incredible Machine und Contraption Maker .
Ein anderer Weg, um Physik zu machen
Ich habe nachgedacht, der Grund, warum sich der kleine Fehler in der Genauigkeit von Gleitkommazahlen verstärkt, ist, dass wir Iterationen mit diesen Zahlen durchführen. In jedem Simulationsschritt nehmen wir die Ergebnisse des letzten Simulationsschritts und bearbeiten sie. Häufen sich Fehler auf Fehler. Das ist dein Schmetterlingseffekt.
Ich glaube nicht, dass wir einen einzelnen Build mit einem einzelnen Thread auf demselben Computer sehen werden, der bei derselben Eingabe unterschiedliche Ausgaben liefert. Aber auf einer anderen Maschine könnte es sein, oder ein anderer Build könnte es.
Es gibt ein Argument, um dort zu testen. Wenn wir genau entscheiden, wie die Dinge funktionieren sollen, und auf der Zielhardware testen können, sollten wir keine Builds veröffentlichen, die ein anderes Verhalten aufweisen.
Es gibt jedoch auch ein Argument dafür, nicht wegzuarbeiten, das so viele Fehler ansammelt. Vielleicht ist dies eine Gelegenheit, Physik auf eine andere Art und Weise zu betreiben.
Wie Sie vielleicht wissen, gibt es eine kontinuierliche und eine diskrete Physik. Beide arbeiten daran, wie weit jedes Objekt im Zeitschritt vorrücken würde. Die kontinuierliche Physik hat jedoch die Möglichkeit, den Zeitpunkt der Kollision herauszufinden, anstatt verschiedene mögliche Zeitpunkte zu untersuchen, um festzustellen, ob eine Kollision stattgefunden hat.
Daher schlage ich Folgendes vor: Verwenden Sie die Techniken der kontinuierlichen Physik, um herauszufinden, wann die nächste Kollision jedes Objekts mit einem großen Zeitschritt stattfinden wird, der viel größer ist als der eines einzelnen Simulationsschritts. Dann nehmen Sie den nächsten Kollisionszeitpunkt und finden heraus, wo sich in diesem Moment alles befinden wird.
Ja, das ist viel Arbeit in einem einzigen Simulationsschritt. Das bedeutet, dass die Simulation nicht sofort startet ...
... Sie können jedoch die nächsten Simulationsschritte simulieren, ohne jedes Mal die Kollision zu überprüfen, da Sie bereits wissen, wann die nächste Kollision stattfinden wird (oder dass im großen Zeitschritt keine Kollision auftritt). Darüber hinaus sind die in dieser Simulation akkumulierten Fehler irrelevant, da wir, sobald die Simulation den großen Zeitschritt erreicht, nur die zuvor berechneten Positionen platzieren.
Jetzt können wir das Zeitbudget verwenden, das wir verwendet hätten, um jeden Simulationsschritt auf Kollisionen zu überprüfen, um die nächste Kollision nach der gefundenen zu berechnen. Das heißt, wir können voraussimulieren, indem wir den großen Zeitschritt verwenden. Unter der Annahme, dass der Umfang einer Welt begrenzt ist (dies funktioniert nicht für große Spiele), sollte es eine Warteschlange zukünftiger Zustände für die Simulation geben, und dann sollte jeder Frame, den Sie gerade interpolieren, vom letzten Zustand zum nächsten.
Ich würde für Interpolation argumentieren. Da es jedoch Beschleunigungen gibt, können wir nicht einfach alles auf die gleiche Weise interpolieren. Stattdessen müssen wir unter Berücksichtigung der Beschleunigung jedes Objekts interpolieren. In diesem Fall können wir die Position genauso aktualisieren wie für den großen Zeitschritt (was auch bedeutet, dass dies weniger fehleranfällig ist, da wir nicht zwei verschiedene Implementierungen für dieselbe Bewegung verwenden würden).
Hinweis : Wenn wir diese Gleitkommazahlen verwenden, löst dieser Ansatz nicht das Problem, dass sich Objekte unterschiedlich verhalten, je weiter sie vom Ursprung entfernt sind. Es ist zwar richtig, dass die Präzision verloren geht, je weiter Sie sich vom Ursprung entfernen, aber das ist immer noch deterministisch. In der Tat, das ist der Grund, warum ich das ursprünglich nicht erwähnt habe.
Nachtrag
Vom OP im Kommentar :
Die Idee ist, dass die Spieler ihre Maschinen in einem bestimmten Format (wie xml oder json) speichern können, so dass die Position und Rotation der einzelnen Teile aufgezeichnet werden. Diese XML- oder JSON-Datei wird dann verwendet, um das Gerät auf dem Computer eines anderen Players zu reproduzieren.
Also kein Binärformat, oder? Das bedeutet, dass wir uns auch Sorgen machen müssen, ob die wiederhergestellten Gleitkommazahlen mit dem Original übereinstimmen oder nicht. Siehe: Überarbeitete Float-Präzision: Portabilität mit neun Stellen