Entwicklung & Code
Game Loops: Spiele und Animationen richtig in Takt bringen
Erfahrene Entwicklerinnen und Entwickler kennen verschiedene Paradigmen, mit Ereignissen umzugehen: Request-Response-Zyklen in Webservern, Event-Handler in GUI-Anwendungen oder die sequenzielle Verarbeitung in CLI-Tools. All diese eventgetriebenen Ansätze haben eines gemeinsam: Sie reagieren auf externe Ereignisse und kehren danach in einen Wartezustand zurück. Ein Webserver wartet auf HTTP-Requests, eine Desktopanwendung auf Mausklicks oder Tastatureingaben.
Weiterlesen nach der Anzeige
Golo Roden ist Gründer und CTO von the native web GmbH. Er beschäftigt sich mit der Konzeption und Entwicklung von Web- und Cloud-Anwendungen sowie -APIs, mit einem Schwerpunkt auf Event-getriebenen und Service-basierten verteilten Architekturen. Sein Leitsatz lautet, dass Softwareentwicklung kein Selbstzweck ist, sondern immer einer zugrundeliegenden Fachlichkeit folgen muss.
Bei der Spieleentwicklung funktionieren diese Paradigmen jedoch nicht. Ein Spiel muss kontinuierlich aktiv sein, auch wenn Spieler gerade keine Eingaben machen. Objekte bewegen sich weiter, Animationen laufen, Gegner agieren, physikalische Simulationen berechnen die nächsten Zustände. Die Spielwelt existiert unabhängig von Benutzerinteraktionen und entwickelt sich permanent weiter.
Diese fundamentale Anforderung führt zum Konzept Game Loop: einer Endlosschleife, die kontinuierlich den Spielzustand aktualisiert und das Ergebnis auf den Bildschirm rendert. Während ein Webserver zwischen Requests untätig ist, läuft eine Game Loop ununterbrochen mit idealerweise sechzig oder mehr Iterationen pro Sekunde.
Auch für Entwicklerinnen und Entwickler außerhalb der Spielebranche bietet das Konzept interessante Perspektiven. Echtzeitvisualisierungen, Simulationen, IoT-Devices oder Robotik-Software teilen viele Anforderungen mit Spielen: kontinuierliche Verarbeitung, präzises Timing und Reaktion auf Inputs bei laufendem System. Die im Folgenden diskutierten Patterns sind direkt übertragbar.
Das grundlegende Konzept der Game Loop
Eine Game Loop besteht im Kern aus drei Phasen, die sich wiederholen: Eingaben verarbeiten, den Spielzustand aktualisieren und das Bild rendern. In der ersten Phase liest die Engine Tastatureingaben, Mausbewegungen oder Controller-Aktionen ein. Die zweite Phase berechnet die Konsequenzen: Charaktere bewegen sich, Projektile fliegen, Kollisionen ereignen sich. Die dritte Phase visualisiert den aktualisierten Zustand.
Dieser Ablauf unterscheidet sich grundlegend von eventgetriebenen Architekturen. Während dort ein Button-Click-Handler aufgerufen wird, seine Aufgabe erledigt und anschließend die Kontrolle zurückgibt, behält die Game Loop permanent die Kontrolle über den Programmfluss. Sie ist die oberste Instanz, die alle anderen Systeme orchestriert.
Weiterlesen nach der Anzeige
Die konzeptionell einfachste Implementierung sieht in Pseudocode so aus:
func main() {
initialize()
for {
processInput()
updateGameState()
render()
}
}
Dieser naive Ansatz hat ein gravierendes Problem: Er läuft nur so schnell, wie die Hardware es zulässt. Auf einem leistungsstarken Rechner könnte die Schleife tausende Male pro Sekunde laufen, auf schwächerer Hardware vielleicht nur dreißig Mal. Das führt zu inkonsistentem Spielverhalten.
Das Problem der unkontrollierten Geschwindigkeit
Angenommen, eine Spielfigur soll sich mit fünf Metern pro Sekunde nach rechts bewegen. Jeder Schleifendurchlauf erhöht die Position um einen festen Wert. Wenn die Schleife auf Computer A tausendmal pro Sekunde läuft, auf Computer B aber nur sechzig Mal, bewegt sich der Charakter auf A mehr als sechzehnmal so schnell – ein inkonsistentes, völlig unspielbares Verhalten.
Dieses Problem betrifft nicht nur Bewegungen, denn die Loop-Frequenz beeinflusst auch Physikberechnungen, Animationen und Timings für Spielmechaniken – also alle zeitabhängigen Vorgänge. Ältere Spiele aus den 1980er und frühen 1990er Jahren litten massiv unter diesem Effekt. Viele DOS-Spiele sind auf modernen Systemen unbenutzbar, weil sie mit mehreren tausend FPS und damit 100-fach zu schnell ablaufen.
Die Lösung liegt in der expliziten Kontrolle der Ausführungsgeschwindigkeit. Die Game Loop muss die Zeit berücksichtigen und ihre Berechnungen entsprechend anpassen. Es gibt dafür verschiedene Ansätze mit unterschiedlichen Trade-offs.
Fixed Time Step: Die einfache Lösung
Der naheliegendste Ansatz, der Fixed Time Step, ist die Begrenzung der Loop-Frequenz auf einen festen Wert, typischerweise 60 FPS (Frames per Second). Nach jedem Schleifendurchlauf wird die verstrichene Zeit gemessen. Wenn der Durchlauf weniger als 16,67 Millisekunden (1000 ms / 60) gedauert hat, wartet das Programm die restliche Zeit ab:
targetFrameTime := 16.67 // Millisekunden für 60 FPS
for {
frameStart := getCurrentTime()
processInput()
updateGameState()
render()
frameTime := getCurrentTime() - frameStart
if frameTime < targetFrameTime {
sleep(targetFrameTime - frameTime)
}
}
Bei diesem Ansatz kennen alle Berechnungen die exakte Zeitdauer eines Frames. Eine Bewegung von 5 Metern pro Sekunde bedeutet 5/60 = 0,0833 Meter pro Frame. Diese Konstanz vereinfacht die Spiellogik erheblich, da sie keine variablen Zeitwerte berücksichtigen muss.
Der Fixed Time Step führt zu deterministischem Verhalten. Bei identischen Eingaben produziert das Spiel auf unterschiedlicher Hardware exakt die gleichen Ergebnisse. Das ist besonders wichtig für Multiplayer-Spiele, Replays oder Debugging. Physik-Engines arbeiten ebenfalls lieber mit konstanten Zeitschritten, da variable Schrittweiten zu numerischen Instabilitäten führen können.
Allerdings hat dieser Ansatz einen fundamentalen Nachteil: Wenn ein Frame länger als die vorgesehene Zeit benötigt, bricht das Konzept zusammen. Auf schwächerer Hardware oder bei komplexen Szenen dauert die Berechnung vielleicht 25 Millisekunden statt 16,67. Das Spiel kann dann die Ziel-Framerate nicht halten und läuft in Zeitlupe ab – ein häufiges Phänomen bei anspruchsvollen Spielen auf Low-End-Systemen.
Zudem verschwendet das aktive Warten (Busy Waiting) CPU-Ressourcen. Moderne Sleep-Funktionen haben außerdem keine Millisekunden-Präzision, was zu Frame-Pacing-Problemen führen kann. Dennoch verwenden einige Spiele, besonders ältere oder retro-inspirierte Titel, diesen Ansatz wegen seiner Einfachheit.
Variable Time Step: Flexibilität mit Risiken
Eine flexiblere Lösung ist der Variable Time Step: Er misst die tatsächlich vergangene Zeit zwischen Frames und verwendet diesen Wert (Delta Time) für alle Berechnungen. Statt mit festen Werten zu rechnen, wird jede Veränderung mit der Delta-Zeit multipliziert. Eine Geschwindigkeit von 5 m/s wird zu position += 5.0 * deltaTime.
lastTime := getCurrentTime()
for {
currentTime := getCurrentTime()
deltaTime := currentTime - lastTime
lastTime = currentTime
processInput()
updateGameState(deltaTime)
render()
}
In diesem Modell passt sich das Spiel automatisch an die Hardwareleistung an. Ein schneller Computer produziert mehr Frames pro Sekunde mit kleineren Delta-Time-Werten, ein langsamer Computer weniger Frames mit größeren Deltas. Die Bewegungsgeschwindigkeiten bleiben konsistent, da 5 m/s * 0,016s und 5 m/s * 0,033s über mehrere Frames hinweg zum gleichen Ergebnis führen.
Der Variable Time Step hat jedoch ein gefährliches Problem: Er kann bei niedrigen Frame-Raten instabil werden. Wenn ein komplexer Frame 50 Millisekunden benötigt, ist deltaTime entsprechend groß. Physikberechnungen mit großen Zeitschritten sind numerisch ungenauer und können zu unplausiblen Ergebnissen führen – Objekte durchdringen sich, Kollisionen werden übersehen, die Simulation läuft komplett aus der Kontrolle.
Diese Instabilität führt zu noch komplexeren Berechnungen im nächsten Frame, was deltaTime weiter erhöht. Es entsteht eine positive Rückkopplung: Große Zeitschritte führen zu Instabilität, was mehr Rechenaufwand nach sich zieht, der zu noch größeren Zeitschritten führt. Dieses Phänomen wird als Spiral of Death bezeichnet und kann ein Spiel komplett unbenutzbar machen.
Zusätzlich erschwert der Variable Time Step das Testen und Debuggen. Unterschiedliche Hardware produziert leicht unterschiedliche Ergebnisse, was Race Conditions und Timing-abhängige Bugs schwer reproduzierbar macht. Multiplayer-Spiele haben mit diesem Ansatz erhebliche Synchronisationsprobleme, da verschiedene Clients unterschiedliche Zeitschritte verwenden.
Trotzdem nutzen viele moderne Spiele Variable Time Steps, weil sie mit Hardwarevielfalt besser umgehen können als Fixed Time Steps. Der nächste Schritt zeigt, wie sich die Vorteile beider Ansätze kombinieren lassen.