Lesen
Frameunabhängig programmieren
Geschrieben am 2010-01-26 14:48:42
Frameunabhängig programmieren beschreibt eine Methode, die sicherstellt, dass bestimmte Prozesse in einer Anwendung stets gleich schnell ablaufen. Unabhängig von der eingesetzten Hardware bzw. der tatsächlichen Ablaufgeschwindigkeit.
Das findet vorallem in Spielen Anwendung, wo die FPS (Frames Per Second, dt. Durchläufe pro Sekunde) unter Anderem stark varrieren können. Ich musste mir heute den Kopf darüber zerbrettern, also dachte ich, dass es vielleicht für Andere hilfreich sein könnte.
Es gibt viele Verfahren, um frameunabhängig zu programmieren. Und zu meinem Erstaunen findet man ziemlich viel Müll darüber im Netz. :-)
Die Abhängigkeit Zeit
Einige Programmierer verlassen sich bei ihren Berechnungen vollständig auf die Zeit. Das ist in der Theorie auch vollständig richtig, denn schließlich ist Bewegung abhängig von der Zeit. Allerdings muss immer bedacht werden, dass so ein Computer in einer Sekunde schrittartig Berechnungen durchführen muss. So als ob man ein paar mal pro Sekunde die Welt einfrieren würde, um alles bewegen zu können.
Also, setzen wir Bewegungen abhängig von der Zeit, werden Objekte schneller bewegt, wenn die Frame-Zeit ansteigt, was durch folgende Gleichung logisch ist:
Weg = Geschwindigkeit * Zeit
Einigen kommt diese Gleichung sicher bekannt vor, wo es doch eine physikalische ist. :-) Wie dem auch sei: Folgende Situation sei gegeben:

Der linke Kreis soll so bewegt werden, dass er die Position des grauen Kreises einnimmt. Blöderweise befindet sich dazwischen aber eine Mauer!
Mit der reinen Zeit-abhängigen Methode würde die Mauer ab einer bestimmten, zu niedrigen FPS einfach übersprungen werden. Dazu ein Rechenbeispiel: Der Kreis hat eine Geschwindigkeit von 1 / 60 LE/s. Und weil das Spiel gerade mit 60 FPS läuft, bekämen wir:
Weg = (1 / 60) LE/s * (1 / 60) s = 1 / 60 LE
Pro Sekunde bewegt sich das Teil also um 1 LE. Wichtiger aber: Pro Durchlauf nur um 1 / 60 LE. Damit können wir problemlos auf eine Kollision mit der Mauer testen. Erhöhen wir nun die Geschwindigkeit auf 30 LE/s und verringern die FPS gedacht auf 20 – nicht ungewöhnlich, viele Leute können aktuelle Spiele nur mit etwa 20 FPS spielen! Das ergibt nun:
Weg = 30 LE/s * (1 / 20) s = 30 / 10 LE = 3 LE
Der Kreis bewegt sich nun ganze 3 LE pro Durchgang! Und das kann wirklich zu ernsthaften Problemen führen, wenn wir annehmen, dsas der Kreis bei Position 0 sitzt und die Mauer bei Position 1. In einem Durchgang nämlich würde der Kreis sprungartig von Position 0 auf Position 3 springen!
Fazit: Verfahren ist durchgefallen, wir brauchen was Besseres. (Übrigens ist es genau dieses Verfahren, was viele einsetzen und sich dann meinen zu helfen, indem sie sich z.B. mit Interpolationen das Leben selbst erschweren)
Die Abhängigkeit Zeit teilen
Zugegeben, der Ansatz war gar nicht so verkehrt. Allerdings müssen wir sicherstellen, dass die abgelaufene Frame-Zeit in kleinere Einheiten unterteilt wird. Wenn wir das obige Beispiel nochmals anschauen und uns vorstellen, dass die Bewegung von 3 LE in dem Durchlauf einfach in mehrere (genügend) kürzere aufgeteilt worden wäre, hätten wir eine Kollision sicher bemerkt.
Was wir nun tun ist uns vorher eine fixe Zeit auszudenken, mit der Aktualisierungen der Bewegung mindestens durchgeführt werden müssen. Das ist stark abhängig von der Anwendung bzw. dem Spiel, das du baust. Bedenken musst du hier: Je mehr du Zeiteinheiten unterteilst, umso häufiger müssen deine Berechnungen durchgeführt werden. Wählst du die Mindestzeit zu hoch, erhälst du wieder ein sprunghaftes Verhalten. Der korrekte Wert hängt also von der Performance und der Höchstgeschwindigkeit von Objekten ab (ab irgendeiner Geschwindigkeit springt es nämlich so oder so, hier brauchst du wirklich Interpolation).
Genug des Blablas, schauen wir uns etwas Pseudocode dazu an:
Mindestzeit = 1 / 60
Hauptschleife() {
Restzeit = LetzteFramezeit;
Solange( Restzeit > 0 ) {
Wenn( elapsed >= Mindestzeit ) {
Bewegen( Mindestzeit )
Restzeit = Restzeit - Mindestzeit
}
Ansonsten {
Bewegen( Restzeit )
Restzeit = 0
}
}
}
Bewegen( Zeit ) {
NachRechts( 10 * Zeit )
}
Zur Erläuterung belasse ich es einfach mal bei folgender Liste:
- Zunächst setzen wir eine konstante Mindestzeit (hier: 60 FPS).
Hauptschleifeist die Mainloop, die nun durchlaufen wird.Restzeitwird auf den Zeitwert gesetzt, den die Hauptschleife beim vorigen Durchlauf benötigt hat.- Nun durchlaufen wir eine weitere Schleife, die solange die Berechnungen aktualisiert, bis keine Restzeit mehr vorhanden ist.
- Wenn die Restzeit höher ist als die Mindestzeit, aktualisieren wir die Berechnungen mit der Mindestzeit als verstrichene Zeit.
- Ist die Restzeit geringer, wird diese als Referenzzeit für die Berechnungen übergeben.
Es fällt hier auf, dass Bewegen() niemals mit einer Zeit höher als Mindestzeit aufgerufen wird! Sofern die Bewegungen also nicht zu groß sind, bist du vollständig auf der sicheren Seite. Läuft das Spiel zu langsam, wird Bewegen() einfach häufiger aufgerufen. Läuft alles zu schnell, wird Bewegen() nur einmal und mit einem entsprechend niedrigen Zeitwert aufgerufen.
Das war's soweit, viel Spaß damit. :-)