framerate-unabhängige GameLoop

Hier könnt Ihr gute, von Euch geschriebene Codes posten. Sie müssen auf jeden Fall funktionieren und sollten möglichst effizient, elegant und beispielhaft oder einfach nur cool sein.
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

framerate-unabhängige GameLoop

Beitrag von ZeHa »

Servus,

die Spieleprogrammierer unter euch kennen sicherlich das leidliche Thema mit der Framerate-unabhängigen Programmierung. In den meisten Tutorials, die ich bisher gesehen habe, wird im Grunde folgender Ansatz erklärt:
1. rendern
2. schauen, wie viel Zeit vergangen ist
3. updaten: Spielobjekte bewegen, Zeitunterschied * Geschwindigkeit
4. goto 1

Dies funktioniert zwar, ist aber meiner Meinung nach etwas umständlich und auch fehleranfällig. Wenn beispielsweise eine Figur auf eine Mauer zuläuft, und der nächste Render-Schritt aus irgendeinem Grund sehr lange dauern sollte (z.B. weil der Rechner im Hintergrund gerade Mails abruft etc), dann kann es (je nach Implementierung der Kollisionsabfrage) passieren, daß die Figur sich im nächsten Frame hinter der Mauer befindet - was jedoch eigentlich gar nicht möglich sein dürfte.
Zudem wird viel mit unvorhersehbaren Komma-Werten gearbeitet usw., was auch immer wieder mal zu Problemen führen kann (z.B. auch bei der Fehlersuche).

Der meiner Meinung nach bessere Ansatz ist der, für die Positions-Updates usw. eine fixe Framerate einzuführen, die auf jeden Fall erfüllt wird, und immer nur so oft zu rendern, wie es möglich ist. Da die Updates i.d.R. nur einen Bruchteil der Render-Zeit benötigen, ist es dann auch kein Problem, nach einem etwas längeren Render-Vorgang einfach die verlorenen Updates nachzuholen.

Das sieht dann grob erklärt ungefähr so aus:
1. rendern
2. warten bis bzw. schauen ob Updates fällig sind
3. updaten: Spielobjekte bewegen, und zwar um einen fixen Wert (anhand der Geschwindigkeit)
4. je nach Zeitunterschied Schritt 3 noch ein paar mal aufrufen
5. goto 1

Die GameLoop, die ich momentan verwende, sieht daher in etwa so aus:

Code: Alles auswählen

running = #True
frameTime = ElapsedMilliseconds()
#DELTA_TIME = 1000 / 60                             ; wir legen uns fest auf 60 Updates pro Sekunde

While running
    currentTime = ElapsedMilliseconds()
    
    If currentTime - frameTime < #DELTA_TIME
        Continue                                    ; die Zeit ist noch nicht gekommen
    EndIf
    
    While currentTime - frameTime >= #DELTA_TIME
        frameTime = frameTime + #DELTA_TIME         ; jetzt holen wir alle nötigen Updates nach
        
        controls()
        update()
    Wend
    
    render()                                        ; und schließlich wird gerendert
Wend
Ob dies der Weisheit letzter Schluß ist, weiß ich nicht, kann gut sein daß es noch Dinge zu Verbessern gibt. Ansonsten viel Spaß bei der Verwendung. Sollte es hierzu schon Tutorials geben, bitte auch Bescheid sagen :)

EDIT: Um es nochmals auf den Punkt zu bringen: Die Loop simluiert quasi eine "fixe Framerate", auch wenn das Rendern zu lange (oder zu kurz) dauern sollte. Dadurch kann man als Programmierer seine Objekte so bewegen, wie man es auch sonst mit einer fixen Framerate tun würde.
Zuletzt geändert von ZeHa am 03.09.2011 11:28, insgesamt 1-mal geändert.
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: meine derzeitige GameLoop

Beitrag von ZeHa »

Man könnte noch drüber diskutieren, ob man die controls() parallel mit den Updates aufruft oder nur mit dem Rendern. Ich bin der Meinung, daß das im Endeffekt relativ wurscht sein dürfte, aber vielleicht hat ja jemand ein paar Argumente für und gegen ;)
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
Vermilion
Beiträge: 1846
Registriert: 08.04.2006 16:00
Computerausstattung: Apple iMac (2010) & HP Notebook
Wohnort: Heidekreis

Danke

Beitrag von Vermilion »

Du hast mich auf etwas aufmerksam gemacht, an das ich vorher nicht gedacht habe (die Sache mit der Mauer), Danke. :)
Bild

Immer die neueste PureBasic Version. Auf allem Betriebssystemen. Ich bin ein OS-Nomad!
Benutzeravatar
AND51
Beiträge: 5220
Registriert: 01.10.2005 13:15

Re: meine derzeitige GameLoop

Beitrag von AND51 »

Warum lagert man nicht alles in Threads aus? Mir fiel mal folgende Idee ein:

Was hat man so alles in einem Spiel? Erstens, klar das darzustellende Bild mit allen Sprites usw. Zweitens, die Benutzereingaben per Maus und Tastatur. Drittens, die Update-Schleife, wo man Spielobjekte bewegt usw. Viertens, Netzwerk-Übertragungen bei Multiplayer-Spielen. Fünftens, Sound und Musik. Sechstens, Krimskrams wie Festplatten-Zugriffe, wenn man laufend irgendwelche Spielstände speichern muss etc.

Ich bin kein Experte in Sachen Multithreading, denke aber, dass dieser Ansatz viele Vorteile bietet:

Threads laufen unabhängig voneinander und praktisch alle Computer haben mehrere Kerne, sodass man die volle Leistung eines PCs erhält. Die Threads würden über globale Variablen, Arrays und LinkedLists miteinander kommunizieren. Wichtig: Das alles muss natürlich mit Mutex und Semaphores abgesichert werden!

Man ist so extrem flexibel: Der erste Thread rendert das Bild und nichts anderes. Er könnte bei 60 Frames also 1000/60=16,6 Millisekunden Pause machen. Er braucht nur die globalen Variablen auslesen und die Figuren etc. darstellen. Der zweite Thread würde laufend die Variablen beschreiben und dazu Benutzereingaben sogar in eine LinkedList für die anderen Threads puffern. Der dritte Thread, updated nur die Variablen und führt weitere Berechnungen aus (wie viel Damage verursacht, Wegfindungs-Routinen, etc.). So würde ich versuchen, ein Spiel zu programmieren. Wieso alles in eine Schleife packen? Der kleinste gemeinsame Nenner wäre dann der Bilddarstellungsteil. Da er nur alle 16 ms aufgerufen werden soll (man beachte ZeHas #DELTA_TIME und Continue), würden somit auch die Benutzereingaben und Netzwerk-Übertragungen zwangsweise alle 16 ms abgerufen. Suboptimal, wie ich finde.

Aber es gibt noch mehr Vorteile! Man könnte die Prioritäten der einzelnen Threads ändern. So ist der Render- und der Update-Thread wichtig (ThreadPriority(30)), der Festplatten-Thread dagegen eher unwichtig (ThreadPriority(12)). Ich würde alle Daten mit LinkedLists oder Arrays puffern und mit Mutexe absichern. So stört es keinen, wenn ein Spielstand langsam auf die Platte geschrieben wird, aber der Render-Thread schnell weiterlaufen muss. Man könnte auch einzelne Threads deaktivieren. Wenn der Benutzer kein Sound/Musik will, dann deaktiviere ich den Sound-Thread. So würde die Sound-Umgebung gar nicht erst initialisiert werden, was Speicher spart. Oder wenn der Benutzer keine Sound- oder Netzwerk-Karte hat, dann kann das Programm trotzdem laufen: Man braucht nur alles Threads zu de-aktivieren, die nicht überlebenswichtig sind.
PB 4.30

Code: Alles auswählen

Macro Happy
 ;-)
EndMacro

Happy End
Benutzeravatar
ts-soft
Beiträge: 22292
Registriert: 08.09.2004 00:57
Computerausstattung: Mainboard: MSI 970A-G43
CPU: AMD FX-6300 Six-Core Processor
GraKa: GeForce GTX 750 Ti, 2 GB
Memory: 16 GB DDR3-1600 - Dual Channel
Wohnort: Berlin

Re: meine derzeitige GameLoop

Beitrag von ts-soft »

@AND51
Wenn sich der Hintergrund bewegt, der Spieler bewegt, alles gleichzeitig, dann wird es kaum möglich sein,
akkurate positionen für die Collosionsprüfung zu ermitteln, da diese Werte ja immer von einem Thread gelocked sind und nach der Freigabe bereits nicht mehr stimmen.

Ich denke das Threads nur beschränkt nutzbar sind, also nicht in der von Dir beschriebenen Form.

Gruß
Thomas
PureBasic 5.73 LTS | SpiderBasic 2.30 | Windows 10 Pro (x64) | Linux Mint 20.1 (x64)
Nutella hat nur sehr wenig Vitamine. Deswegen muss man davon relativ viel essen.
Bild
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: meine derzeitige GameLoop

Beitrag von ZeHa »

Die einzigen Dinge, die man bei der Spieleprogrammierung in Threads packt, sind z.B. Netzwerk- oder Sound-Sachen. Das Rendern und Updaten ist parallel schlichtweg nicht möglich, da sonst beispielsweise Grafikfehler entstehen (noch nicht alle Figuren sind an der richtigen Position, es wird aber schon gerendert). Wenn man dann alles per Mutex lockt, hat man am Ende wieder genau das gleiche wie anfangs, nur mit dem Unterschied, daß man alles schön kompliziert in Threads gepackt hat.

Das einzige, was vielleicht noch denkbar wäre, sind komplexere AI-Berechnungen. Wenn z.B. bei einem Strategiespiel eine komplexere Taktik berechnet werden muß, welche vielleicht sogar ein paar Sekunden dauern kann, dann kann man das einfach in einem Thread machen und erst wenn diese Berechnung fertig ist, fangen die Einheiten entsprechend an, zu marschieren.

Aber ansonsten sollte man bei der Spieleprogrammierung (und eigentlich auch in jedem anderen Bereich) so gut es geht auf Threads verzichten.
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
AND51
Beiträge: 5220
Registriert: 01.10.2005 13:15

Re: meine derzeitige GameLoop

Beitrag von AND51 »

Doch, es ist bestimmt möglich. Vielleicht habe ich in meiner Aufzählung oben zu stark die Sachen separiert.

Dann würde ich eben den Update-Thread die Benutzereingaben registrieren und gleichzeitig alle Variablen aktualisieren lassen (die Kollisionsprüfung ist also komplett im Update-Thread). Dann ist der Thread in der Lage zu bemerken, dass ich gar nicht weiter nach links gehen kann, obwohl ich Pfeil_Links drücke. Je nach Programmierweise ändert der Update-Thread die variable erst gar nicht, oder er setzt sie auf den früheren Wert zurück. Der Render-Thread an sich braucht ja nichts weiter zu tun, als alle paar Millisekunden die Variablen einmal auszulesen. Von mir aus vorher mittels Mutex locken, denn sonst hast du Recht: Der Render-Thread könnte versehentlich die Y-Position auslesen, während die X-Position gerade aktualisiert wird. Ich denke aber, dass der Render-Thread weit weniger als 1 ms braucht, um sich eben alle wichtigen Variablen zu kopieren und anzuzeigen. Und diesen Bruchteil einer Millisekunde kann der Update-Thread bestimmt verschmerzen, zumal er in dieser Zeit mit TryLockMutex() auch andere Berechnungen anstellen kann, wenn er merkt, dass er nicht schreiben kann.

Wie gesagt, es kommt auch auf die Art und Weise an, wie man als Programmierer alle Threads miteinander umgehen lässt. Ich hoffe, ich habe dich richtig verstanden und könnte dir eine zufriedenstellende Lösung beschreiben?



@ Zeha
Ok, das leuchtet ein. Dann würde ich von mir aus die Render-Routine auch noch in den Update-Thread packen. Dass man aber möglichst überall auf Threads verzichten sollte, dem widerspreche ich eindeutig! Wozu haben denn Prozessoren mehrere Kerne und Hyperthreading? Und wenn ich mir so manches Spiel im Taskmanager anschaue, haben die auch dutzende Threads. Zu irgendwas müssen die ja gut sein. Aber wie gesagt, ich bin kein Spiele-Experte! Ich programmiere eher Anwendungen. Also bitte nicht hauen^^
PB 4.30

Code: Alles auswählen

Macro Happy
 ;-)
EndMacro

Happy End
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: meine derzeitige GameLoop

Beitrag von ZeHa »

Dass man aber möglichst überall auf Threads verzichten sollte, dem widerspreche ich eindeutig! Wozu haben denn Prozessoren mehrere Kerne und Hyperthreading?
Naja zum einen ist Multicore und Hyperthreading ja bereits dafür sinnvoll, daß man mehrere Programme gleichzeitig laufen lassen kann, diesen Vorteil gibt es also immer, auch bei singlethreaded Programmen.

Und natürlich ist es zum Teil auch sinnvoll, innerhalb eines Programms mehrere Threads zu verwenden - aber eben wirklich nur dann, wenn es notwendig ist. Wenn etwas ohne Threads genausogut funktioniert, dann sollte man dies eigentlich auch immer so lösen, da erstens der Code deutlich lesbarer ist, man nicht ständig aufpassen muß, daß man Variablen korrekt lockt, und die Deadlock-Gefahr ist auch deutlich geringer :mrgreen:

Also nicht falsch verstehen: Threads sind nicht böse und man sollte sie auch durchaus verwenden, aber halt immer nur da wo sinnvoll. Selbst bei Anwendungen! Und sowas wie "Tasten abfragen" - "Positionen anpassen" - "Kollisionen checken" - "Positionen korrigieren" - "den ganzen Kram rendern" ist nunmal ein sehr sequentieller Vorgang, der an keiner Stelle wirklich sinnvoll parallelisierbar ist. Sound, Netzwerk, Savegames, dynamisches Nachladen, etc, das mag alles im Hintergrund funktionieren (vor allem auch weil diese Dinge zum Teil länger brauchen als ein Update/Render-Zyklus), aber die eigentliche GameLoop sollte immer im gleichen Thread ablaufen.
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: meine derzeitige GameLoop

Beitrag von ZeHa »

Vielleicht noch ein simples Beispiel: Einkaufen

- Einkaufszettel schreiben
- zum Supermarkt laufen
- Sachen aus dem Regal nehmen
- an der Kasse bezahlen
- nach Hause gehen

Man kann jetzt zwar einen Schreib-Thread, einen Lauf-Thread, einen Nehm-Thread und einen Bezahl-Thread machen, aber die müßten letztendlich sowieso alle auf einander warten. Also lieber alles schön nacheinander erledigen ;)
Was man aber während des gesamten Vorgangs nebenher tun kann:

- mp3-Player hören
- ans Handy gehen falls es klingelt

Also ist der Sound- und der Netzwerk-Thread vielleicht gar nicht vekehrt :mrgreen:

(Anmerkung: da unter PB (und unter vielen anderen Libs) das Abspielen von Sound aber sowieso im Hintergrund geschieht, braucht man sich dafür keinen eigenen Thread bauen, sondern der Aufruf von PlaySound() sorgt bereits von selbst dafür, daß der Sound im Hintergrund läuft)
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
AND51
Beiträge: 5220
Registriert: 01.10.2005 13:15

Re: meine derzeitige GameLoop

Beitrag von AND51 »

Und sowas wie "Tasten abfragen" - "Positionen anpassen" - "Kollisionen checken" - "Positionen korrigieren" - "den ganzen Kram rendern" ist nunmal ein sehr sequentieller Vorgang, der an keiner Stelle wirklich sinnvoll parallelisierbar ist.
Nicht falsch verstehen! Das meinte ich auch gar nicht.

Ich wollte zeigen, dass dieser sequentielle Vorgang, der ja auch logisch zusammen gehört, in einen Thread gepackt werden kann, um ihn so vom restlichen Programm zu lösen. Ich habe nicht gesagt, man soll für jede Taste auf der Tastatur und für jeden Pixel auf dem Bildschirm einen Thread eröffnen (übertrieben ausgedrückt). Vielleicht haben wir auch unterschiedliche Vorstellungen davon, was eine GameLoop ist? Für mich ist die GameLoop der riiiiiiiiesige Repeat : Until quit = 1 Block, wo ein Anfänger aaaaalles rein packen würde, was ein Spiel eben so braucht: Bild, Sound, Netzwerk, Input, ... Wie ich das hier lese, bekomme ich den Eindruck, dass du lediglich die GameLoop meinst, die sich um die Darstellung der Bilder und der korrekten FPS kümmert. In dem Fall haben wir möglicherweise aneinander vorbei gesprochen.

Ich habe ja auch mehrfach betont, dass die Programmierweise wichtig ist. Jemand, der sich gut mit Mutex/Threads auskennt, kriegt das alles locker hin; seine Threads würden zusammen arbeiten, wie die Zahnräder ein Uhrwerk. Das Bild des Uhrwerks ist es, was mir hier vorschwebt, wenn ich die ganze Zeit von Threads rede. Wem Threads nicht so liegen, oder sie nicht so mag, der möge nur elementarste Programmbestandteile wie KI-Berechnungen und Netzwerk-Sachen auslagern.

Dein Argument bezüglich Multicore und mehreren Instanzen und mehreren singlethreaded Programmen kann ich nicht ganz gelten lassen. An sich ist es völlig richtig, was du sagst und ich stimme dir zu. Dieses Arguemt zählt aber nicht immer. Wenn ich ein kleines 640x480-Pacman programmiere, dann bin ich vielleicht daran interessiert, dass mein Spiel des Jahres nicht gleich den PC auslastet. Aber wenn ich mich für einen begnadeten Programmierer halte und Crysis XII programmiere, dann sind mir andere Programme egal. Nur mein Spiel soll ja laufen und der versierte User weiß das. Vielleicht wird er sogar ressourcenfressende Hintergrundprogramme ausschalten, wie auch immer. Fakt ist, dass jemand mein Spiel spielt und in dem Moment mein Programm Vorrang vor allen anderen hat. Und dann — dann macht sich Heavy Threading bezahlt.
PB 4.30

Code: Alles auswählen

Macro Happy
 ;-)
EndMacro

Happy End
Antworten