Unity, DeltaTime, FixedTimestamp und wie man sich selbst reinlegt

Viel zu lange sass ich jetzt an einem Problem, dass ich gerne mit euch teilen möchte. Es geht wieder um das „SpaceshipInTrouble“-Projekt, genauer gesagt um die Steuerung des kleinen Raumschiffes. Hierzu habe ich zwei „analoge Joysticks“ eingebaut (die Knöpfe links und rechts auf dem Screenie). Über den linken steuert man das Schiff, über den rechten kann man die Schuss-Richtung bestimmen. spaceship_progress_01

Um dem Spieler ein angenehmeres und „realistischeres“ Spielverhalten zu ermöglichen, soll das Raumschiff nicht sofort beim Bewegen des Joysticks auf Maximalspeed sein, sondern langsam auf eine Maximalgeschwindigkeit hin beschleunigen. Gleichzeitig soll bei einem Richtungswechsel oder bei einem Stoppen der Beschleunigung das Raumschiff noch ein wenig in die alte Richtung weiterdriften. Trägheit for the win.

Fixed Timestep

Solang man fixe Timesteps hat ist alles gar kein Problem. Unity bietet hierfür neben der „Update“-Methode auch noch die „FixedUpdate“ Methode, in der man sich dann im Prinzip gar nicht mehr um die Timesteps kümmern braucht und ganz unbeschwert Rechnen kann. Wenn man sich Ärger ersparen möchte, sollte man unbedingt diese Methode verwenden.

Aber sie hat auch Nachteile… Dinge wie „Zeitlupe/Zeitraffer“ oder ähnliches sind hier natürlich nicht möglich. Gut bislang hat mein Spiel diese Funktionen auch nicht, aber ich wollte es mir offen halten – und wenn man schon in seiner Freizeit was bastelt, dann doch wenigstens gescheit, oder? Also auf zum variablen Timestep

Variable Timestep

Hier verwendet man einfach die normale Update-Methode und bezieht „Time.deltaTime“ mit in die Berechnungen mit ein. Time.deltaTime ist ein float mit der seit dem letzten Frame verstrichenen Zeit. Wenn man sich an die 10./11. Klasse zurückerinnert (ja lang ist’s her) kommen einem vielleicht die Bewegungsformeln wieder in den Sinn, die man hier rauf- und runterrechnen durfte:

Soweit recht einfach:

Die Position (s) zum Zeitpunkt (t) ist (bei gleichbleibender Geschwindigkeit) die Anfangsposition (s0) addiert mit dem Produkt aus Geschwindigkeit (v) und Zeit (t).

Entsprechend ist die Geschwindigkeit (v) zum Zeitpunkt (t) (bei gleichbleibender Beschleunigung) die Summe aus Anfangsgeschwindigkeit (v0) und dem Produkt aus Beschleunigung und Zeit.

Nun könnte man annehmen, dass die Vereinfachung mit der „gleichbleibenden Geschwindigkeit“ und „gleichbleibenden Beschleunigung“ quatsch ist, da das Raumschiff sich ja in unterschiedlichsten Geschwindigkeiten bewegen können soll. Dem ist aber nicht so: Da wir immer nur für einen Zeitbruchteil die Geschwindigkeit und Position des Schiffs berechnen, können wir ganz grosszügig davon ausgehen, dass sich die Geschwindigkeit und Beschleunigung während der Berechnung selbst nicht ändert. Das heisst in unserem Fall:

Wobei hier shipDirection, position und velocity alle Vector3 sind und acceleration einem Skalarwert entspricht. Und schon haben wir Geschwindigkeit und Position nahezu unabhängig von der aktuellen Framerate.

Reibung / Friction

Leider ist das bei der Reibung nicht ganz so einfach wie bei Geschwindigkeit und Position, da die Reibung selbst wiederum in Abhängigkeit zur Geschwindigkeit steht. In einem „fixed Timestep“-Szenario würde man z.B. einfach folgendes Nutzen:

Im variablen Timestep-Szenario funktioniert dies aber leider nicht. Und nein, man kann nicht einfach Time.deltaTime aufmultiplizieren. Möchte man das ganze physikalisch korrekt berechnen wird das eine extrem kompliziert… Glücklicherweise gibt es jedoch eine (meist) akzeptable Annäherung:

Die Herleitung spare ich mir an der Stelle mal :).

Manchmal sind die einfachsten Probleme die schlimmsten…

…mit denen man dann doch einige Stunden verbringt, weil man den Fehler ganz wo anders vermutet….

Auf dem PC lief mit den oben verwendeten Formeln schliesslich alles rund. Das Schiff beschleunigte, bremste ab, trieb noch ein wenig nach… perfekt. Also das ganze mal auf dem (mittlerweile etwas in die Jahre gekommenen) Google Nexus 7 testen, wie es sich denn mit den On-Screen-Touchjoysticks spielen lässt. Ergebnis: Gar nicht… Das Schiff war vieeel zu schnell.

Warum? Ich habe doch überall Time.deltaTime genutzt und die Formeln müssten doch auch alle stimmen? Nun der Physikunterricht war lange her… haben sich doch Fehler eingeschlichen? Also alle Formeln noch einmal durchgegangen, durchgerechnet, Beispielzahlen eingesetzt… Alles perfekt… Vielleicht die Reibung? Hier habe ich ja nur eine Annäherung benutzt?

Also auf die Suche gegangen, geschaut, ob man die Reibung vielleicht anders Berechnen könnte, usw. Nachdem ich nicht weiter kam dann schliesslich Punkt für Punkt vereinfacht und rausgenommen.

Keine Reibung mehr -> funktioniert immer noch nicht. Keine Beschleunigung mehr (Schiff startet auf 100% speed) -> funktioniert immer noch nicht. Hier war ich mir nun aber wirklich absolut sicher, dass die Formeln stimmten. Also musste der Fehler doch wo anders liegen…

Und siehe da: Ich nutzte folgenden Befehl zum setzen meiner Position…

In der festen Annahme, dass SimpleMove entsprechend die Position ändert (was es fieserweise auch tut – aber ganz anders als erwartet). Ein Blick in die Unity-API-Doc brachte dann den Facepalm und die Lösung:

SimpleMove erwartet eine Geschwindigkeit, nicht eine Position. Es rechnet also selbst sein Time.deltaTime mit ein… Kein Wunder, dass es auf dem langsamen Android mit gefühlt 1/3 bis 1/4 Framerate dann entsprechend 3 bis 4mal so schnell lief, da ich den Zeitfaktor ja einmal zu viel aufmultipliziert hatte.

Auf dem PC ist es nicht aufgefallen, da hier das deltaTime äusserst kontant ist und somit die Geschwindigkeit immer nur um einen gleichbleibenden Faktor verringert hat.

Nächstes mal sollte ich vielleicht eher meinen Formeln vertrauen und gucken, ob das Zeug aussenherum stimmt 🙂 da hatte ich mir diesmal viel Mühe erspart…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.