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:

Re: framerate-unabhängige GameLoop

Beitrag von ZeHa »

Was die Abfrage der Maus angeht, ist das zwar richtig, aber das ordne ich dann auch in der gleichen Schublade ein wie die Sache mit dem Sound- und dem Netzwerk-Thread. Man hat das im Hintergrund laufen, aber in dem Moment, wo man es braucht (im regulären Update- oder eben Render-Prozeß) greift man dann trotzdem auf den aktuellen Wert zu. Das kann man so machen, finde ich auch nicht vekehrt, aber auch das ist jetzt kein "Multithreading-Trick um mehr Performance zu haben" oder sowas ;)

Bei Deinem zweiten Problem weiß ich nicht genau, worauf Du hinauswillst. Meinst Du jetzt, daß das Spiel mal ordentlich stockt (also 5 Sekunden quasi nichts passiert) oder daß die Framerate einfach nur mal 5 Sekunden lang sehr niedrig ist, z.B. 3 FPS oder sowas? Auf jeden Fall kann (und sollte) man für solche Fälle auch eine Lösung finden. Das wäre mit der vorgestellten GameLoop aber auch kein Problem, denn man kann z.B. ein If einbauen, welches checkt, ob man bswp. gerade mehr als 20 Frames verloren hat, und wenn ja, entsprechend reagieren (indem man trotzdem rendert - wodurch es dann während dieser Ruckelperiode halt trotzdem langsamer läuft; oder eben pausieren oder sonstwas). Problematisch kann es dann aber wieder bei Netzwerkspielen werden, wobei das eh wieder so ein Thema für sich ist :)

Also generell gebe ich Dir da Recht, man sollte auf jeden Fall beachten, daß eine zu niedrige Framerate zu Problemen führen kann. Allerdings gibt es ja auch Framerates, bei denen man locker noch spielen kann, und in solch einem Fall funktioniert die GameLoop ausgezeichnet. Sagen wir mal, wir wollen eine Framerate von 60 FPS haben, aber der Rechner schafft es nur, 27 zu rendern. Dann würde trotzdem noch alles recht glatt laufen. Sogar mit 15 FPS wäre es noch spielbar. Und trotzdem brauche ich mir als Programmierer nicht ständig Gedanken darum machen, sondern ich programmiere das Spiel einfach als ein 60 FPS-Spiel und gut ist ;) so wie früher auf dem C64 oder Amiga, wo man ja meist mit der Bildwiederholfrequenz synchronisiert hat. Damals konnte man einfach sagen, okay diese Figur soll jeden Frame 1 Pixel weiterlaufen, und genau das kann man halt auf diese Weise auch erreichen.
Aber so eine Framerate-nach-unten-Begrenzung ist definitiv keine schlechte Idee. Zum Beispiel so:

Code: Alles auswählen

While running
    currentTime = ElapsedMilliseconds()
    
    If currentTime - frameTime < #DELTA_TIME
        Continue                                    ; die Zeit ist noch nicht gekommen
    EndIf

    lostFrameCount = 0
    
    While currentTime - frameTime >= #DELTA_TIME
        frameTime = frameTime + #DELTA_TIME         ; jetzt holen wir alle nötigen Updates nach
        
        controls()
        update()

        lostFrameCount + 1
        If lostFrameCount == 20
            Break
        EndIf
    Wend
    
    render()                                        ; und schließlich wird gerendert
Wend
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
7x7
Beiträge: 591
Registriert: 14.08.2007 15:41
Computerausstattung: ganz toll
Wohnort: Lelbach

Re: framerate-unabhängige GameLoop

Beitrag von 7x7 »

ZeHa hat geschrieben:Damit müssen aber wieder beide Threads aufeinander warten, weil Du ja immer wieder die Daten vom einen Buffer in den nächsten kopieren mußt, damit der andere Thread damit weiterarbeiten kann. Das ganze wird dadurch sogar noch langsamer als wenn es single-threaded wäre.
Genau das, was du da sagst, stimmt eben nicht!
Denk doch nochmal über Threads im allgemeinen nach, und über das, was AND51 in seinem ersten
Beitrag wirklich schön gesagt hat.
- alles was ich hier im Forum sage/schreibe ist lediglich meine Meinung und keine Tatsachenbehauptung
- unkommentierter Quellcode = unqualifizierter Müll
Benutzeravatar
AND51
Beiträge: 5220
Registriert: 01.10.2005 13:15

Re:framerate-unabhängige GameLoop - Jetzt räum ich mal hier

Beitrag von AND51 »

Danke, 7x7 und meine anderen Unterstützer! Ich will hier wohl noch mal einen Versuch wagen, ZeHa von Threads zu überzeugen...
7x7 hat geschrieben:
ZeHa hat geschrieben:Damit müssen aber wieder beide Threads aufeinander warten, weil Du ja immer wieder die Daten vom einen Buffer in den nächsten kopieren mußt, damit der andere Thread damit weiterarbeiten kann. Das ganze wird dadurch sogar noch langsamer als wenn es single-threaded wäre.
Genau das, was du da sagst, stimmt eben nicht!
ZeHa, mir scheint, als hättest du einen Verständnisfehler. Mit Threads ist man möglicherweise gleich schnell oder schneller als Singlethreaded, aber nie langsamer!

Ich beginne mal mit deiner letzten GameLoop. Die scheint mir komisch. Wenn ich sie richtig deute, hälst du alle Updates solange hin, bis mal wieder ein Rendering nötig wird. Wieso? Schau mal, hier eine singlethreaded GameLoop, in der Updates immer Vorrang haben und nicht durch das Rendering (das ja nur alle 16 ms für 60 FPS stattfinden soll) ausgebremst wird:

Code: Alles auswählen

Define lastTime
While running
   currentTime = ElapsedMilliseconds()

   controls()
   update()

   If currentTime - lastTime > #DELTA_TIME    ; #DELTA_TIME sei 16 ms, das entspricht 60 FPS
      lastTime = currentTime
      render()
   EndIf

   Delay(1)
Wend
Darauf bin ich schon als Anfänger bei meinen ersten Spiele-Erfahrungen gekommen. :wink: Du siehst, die ganze while-Schleife wird permanent ausgeführt, natürlich mit einem Delay(1). Doch der render()-Teil wird nur alle 16 ms ausgeführt. Somit haben wir ganz nach deinen Vorlieben eine single-threaded GameLoop, doch mit flüssigen Updates und einer rein theoretisch konstanten FPS-Rate, ohne jedoch die GraKa zum Glühen zu bringen!


Schauen wir uns folgendes an:

Code: Alles auswählen

While running
   console()
   update()
   render()
Wend
Ziemlich simpel, he? Doch dieses Konstrukt ist nur so stark wie sein schwächstes Glied. console()? Nein, die Eingaben dürften schnell eingelesen sein. update() oder render()? Kommt drauf an. Habe ich viele upzudatende Variablen und richtig heavy Wegfindungs-Routinen und ein paar KIs am Start, wird vielleicht update() länger als render() dauern. Hat update() jedoch nicht viel zu tun und es werden trotzdem aufwendige 3D Effekte eingesetzt, dann kann render() länger als update() dauern.

Egal welche Prozedur hier am längsten dauert: Sie bremst alle anderen aus. Auch mein obiger Code mit der 'besseren' while-Schleife ist nicht ganz perfekt, denn auch dort könnte es sein, dass update() immer länger als render() braucht. In dem Fall wäre die If-Abfrage sogar unnütz! Diese Konstruktion lohnt sich nur, wenn die Dauer von console()+update()+netzwerk()+sound()+festplatte() < render(). Ich hoffe, du hast verstanden, was ich meine.


Kommen wir jetzt zu multi-threading. Warum hat das Vorteile gegenüber single-threading? Ganz einfach: Es arbeiten alle Programmteile unabhängig voneinander. Egal ob man Threads über globale Arrays/Listen kommunizieren lässt, wie es mein Vorschlag war ... oder ob der Main Thread die ganze Arbeit an die einzelnen Threads verteilt, wie PMV es macht. Ich erkläre aber mal lieber meinen Ansatz, das kann ich bessser erklären.

So wie man unter Unix Pipes verwendet, um Programme miteinander zu verknüpfen, so verknüpfst du alle Programmteile aka Threads zu einer Kette:

Code: Alles auswählen

ls /etc | grep ".conf" | sort
ls listet den Verzeichnisinhalt auf, übergibt diesen String an grep. grep filtert alles außer conf-Datien und übergibt die gefilterte Liste an sort. sort sortiert die Liste und gibt sie aus.

Code: Alles auswählen

console() -> update() -> render()
console() liest des Benutzers Eingaben und übergibt die Werte an update(). (Ohne Eingabe kann keine Spielfigur bewegt werden und ohne Figur braucht man nichts darzustellen.)
update() wurschtelt herum, berechnet hier und da und übergibt die Positionsvariablen an render(). (Ohne Positionsvariablen braucht man nichts zu rendern.)
render() stellt das Bild dar. Dann fängt's von vorn an.

Man sieht, beim simplen Single-threading ist der Nachfolger vom Vorgänger abhängig. Setzt man das ganze jedoch 1:1 mit Threads um und baut schön Mutexe ein, dann hat man Threads, die aufeinander warten. Hier hast du Recht, ich hab das gleiche Ergebnis wie beim Single-Threading.

Wir wollen aber nicht aufeinander warten. Wenn ein Thread in der Kette fertig ist kann er entweder auf den Vorgänger warten (schlecht) oder mit den alten Daten weiterarbeiten und nochmal durchlaufen (gut). Dazu 3 Beispiele:

1. Betrachten wir console() -> update(). Angenommen, update() sei mal vor console() fertig. Warum sollte es auf neue Eingaben warten? Wenn man eine KI hat, kann die doch gar nicht weiterdenken, sondern muss auch auf console() warten... Würde update() aber einfach weiterlaufen und beim nächsten Durchlauf die Eingaben einlesen, dann könnte die KI weiterdenken; komplexe Wegfindungsroutinen, die viel Zeit brauchen, weiterlaufen; usw.

2. Betrachten wir den Schluss der Kette, update() -> render(). Meist ist es doch so, dass die update()-Routine, besonders bei kleineren Games, schnell durchläuft. Sagen wir in <=8 ms. Die render() Routine hat ein Delay(16) in der Schleife. So erreicht render() eine konstante Framerate! Spätestens hier sollte klar sein: Das Spiel läuft flüssiger, als beim bloßen Single-threading!!! Pro render() können 2x update() ausgeführt werden! Das ist natürlich nur Theorie, denn das Verhältnis 8:16 Millisekunden ist nur ausgedacht. Dennoch gibt's praktisch nur Vorteile: Ist update() schneller, dann "denkt" das Spiel flüssiger. Ist render() schneller, bleibt wenigstens das Bild "scharf", weil immer konstant 60 Frames angezeigt werden können, einige davon jedoch doppelt. Ich denke da z. B. auch an die Vertikale Bildsynchronisation. Siehe den FlipModus bei OpenScreen(). Solltest du #PB_Screen_WaitSynchronization verwenden, brauchst du faktisch kein Delay(16) in der render()-Schleife, da FlipBuffers() dich automatisch ausbremst. Dank der Threads bist du diese Sorgen jedoch los.

3. Je nach Größe des Spiels kann eine GameLoop recht komplex werden. Holen wir doch mal network() und sound() ins Boot (ich lagere sound() analog zu render() aus, über den Sinn lässt sich streiten). Dann sähe die Kette so aus:

Code: Alles auswählen

                              +––> network()
network() –––+                +––> sound()
console() –––+––> update() –––+––> render()
Hier leistet update() wahrlich das meiste! Es sei denn, man kann update() selbst noch irgendwie parallelisieren. Doch der Reihe nach...

Erst mal müssen die Eingaben gelesen werden. Netzwerkdaten der anderen Spieler kommen auch noch rein. Dabei wird network() praktisch immer langsamer als console() sein. Würde man console, network() und update() wie beim single-threading hintereinander schalten, würde network() alles ausbremsen. Gibt es umgekehrt jedoch Hänger im System, weil beispielsweise console() (oder noch ein anderer Thread der jedwede Art von Eingaben einliest) muckt, dann bleiben wenigstens der Datenepmfang und update() unberührt. Fakt ist: update() kann unabhängig von Art, Schnelligkeit und Anzahl der Vorgänger arbeiten! Sollte update() mal keine Daten von einem seiner Vorgänger erhalten haben, rechnet update() einfach mit den alten Daten vom letzten Durchlauf weiter. Dies passiert z. B. bereits dann, wenn der Benutzer einfach mal keine Tasten drückt. In dem Fall rechnet update() mit den letzten Daten weiter und lässt die Spielfigur da stehen wo sie auch vorher schon stand.

Übrigens: Wie kommen die Daten vom Vorgänger zum Nachfolger? Daten weiterreichen oder abholen lassen? Ich persönlich würde das einfacherere Prinzip ausprobieren: Threadsafe an und alle Variablen globalisieren, sodass jeder Thread die Daten des Vorgängers auslesen kann...

Nachdem update() nun fertig ist, brauchen wir uns auch nicht darum sorgen, wann und wie oft die Ergebnisse von network(), sound() und render() abgeholt werden. Bei sound() steckt ja, wie hier angesprochen, nicht viel hinter. Wie gesagt, man könnte dies auch in update() reinpacken. Doch wieso nicht alles konsequent logisch aufteilen? (Wie gesagt, man könnte so den sound()-Thread deaktivieren, wenn Init()-Sound fehlschlägt. Andernfalls müsste man im update()-Thread jeden PlaySound()-Befehl mit If sound : EndIf umgeben.) Dann käme sicherlich render(), welches mehr Zeit als sound() benötigt. Aber: Leistungsschwankungen (Virenscanner, Downloads, Hintergrundprogramme) machen sich bei render() bestimmt eher als bei sound() bemerkbar. Scheiß drauf, wir haben Threads! Dann hat update() halt mal Ergebnisse bereitgestellt bevor render() fertig war und sie abholen konnte. Dann zeichnet render() halt ab dem nächsten Frame wieder mit aktuellen Daten. network() ist hier sicherlich der chaotischste Faktor. Jeder Benutzer hat eine andere Bandbreite, Latenzzeit, Downloads im Hintergrund, usw. Es ist schwer, sich beim Single-threading genau darauf einzustellen; hier jedoch prinzipell vernachlässigbar, weil network() das letzte Glied der Multi-Threading-Kette ist.


Wie erwähnt gibt es weitere Vorteile:
- Man nutzt automatisch mehrere Prozessorkerne aus.
- Oder man kann, wenn man es geschickt anstellt, die Auflösung in-Game ändern: Das InitSprite() steckt ja in render(). Bei einem Auflösungs-Wechsel müsste man also nur diesen Thread neu starten und nicht die ganze Anwendung (Modularisierung). Dadurch spart man sich ein erneutes Aufrufen von InitSound(), InitKeyboard(), InitNetwork(), ...
- Man könnte bei Programmstart ein Intro-Video anzeigen und im Hintergrund läuft ein Thread der schon mal die Sprites lädt.
- Dank ThreadPriority() können Threads unterschiedlich priorisiert werden, damit sie im Zusammenspiel noch flüssiger laufen.

Eine Sache noch: Der Datenaustausch. Natürlich könnte man in den Threadsafe Modus schalten und jeden Thread die globalen Variablen des Vorgängers auslesen lassen. Das würde theoretisch laut Doku gehen, wenn Vorgänger schreibt und gleichzeitig Nachfolger liest. Aber das ist natürlich stümpferhaft, wenn der Nachfolger langsamer liest als der Vorgänger schreibt. Es enstünde beim Nachfolger eine Dateninkonsistenz durch eine Race Condition (richtig so?). Man müsste die Daten mit Mutexe schützen, sodass Daten nur ganz oder gar nicht vom Vorgänger abholt werden können. Dabei würde ein Nachfolger seinen Vorgänger im schlimmsten Fall für die komplette Dauer des Lesevorgangs blockieren. Ich glaube jedoch, dass das nicht weiter tragisch ist, denn das Kopieren/Einlesen der Daten geht sehr schnell. Noch schneller geht es vielleicht mit Methoden wie CopyMemory(). Da werden einem schon raffinierte Methoden einfallen. Man kann das sogar weiter auf die Spitze treiben, indem man von TryLockMutex() Gebrauch macht. So kann ein Thread situationsabhängig gesteuert werden und alternative Aufgaben durchführen, wenn er gerade nicht lesen oder schreiben kann.



So, ich hoffe ich konnte dir, ZeHa, zumindest eine kleine Erleuchtung bringen. Ist doch ganz schön viel geworden. Tippe ja auch seit gut 2 Stunden. :coderselixir: Unbezahlt. Nur für dich, du Sturkopf! :iamwithstupid: (War nur Spaß... Hey, ich wollte den Kaffee-Smiley schon immer mal sinnvoll verwenden :mrgreen: ). Ich hoffe, dieser Beitrag ist auch für andere interessant, die hier schmökern. :D
Gute Nacht, allerseits.
PB 4.30

Code: Alles auswählen

Macro Happy
 ;-)
EndMacro

Happy End
DarkDragon
Beiträge: 6267
Registriert: 29.08.2004 08:37
Computerausstattung: Hoffentlich bald keine mehr
Kontaktdaten:

Re: Re:framerate-unabhängige GameLoop - Jetzt räum ich mal h

Beitrag von DarkDragon »

@AND51:
  • Platformunabhängigkeit ist nicht mehr gegeben, da auf verschiedenen Betriebssystemen verschiedene Scheduler laufen und damit unterschiedlich schnell/langsam sind.
  • Das Problem der framerate-unabhängigen Bewegung ist damit also nicht gelöst, denn der Thread läuft auf verschiedenen Systemen unterschiedlich schnell.
  • Der Aufwand das ganze auf Serialisierbarkeit etc. zu prüfen ist übertrieben für ein kleines Spiel.
  • Deadlocks führen zum langsamsten System überhaupt.
  • (Auch ein Scheduler benötigt Zeit; da dieser jedoch sowieso ständig mit läuft, weil er im Betriebssystem steckt zählt dieses Argument erstmal nicht)
Für mich sind Threads in diesem Zusammenhang keine Alternative, weil sie schlichtweg das Problem nicht lösen und diese nur in einen anderen Thread umziehen. Wenn der Thread dann mal hängt war alles für die Katz, was?

@ZeHa: deins ist aber auch nicht das gelbe vom Ei für mich ;-) . Zeitliches Supersampling ... es gibt immer Zustände, die nicht gesehen werden können.
Angenommen es gäbe einen Algorithmus mit imaginärer Laufzeit O(i * n), dann gilt O((i * n)^2) = O(-1 * n^2) d.h. wenn man diesen Algorithmus verschachtelt ist er fertig, bevor er angefangen hat.
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: framerate-unabhängige GameLoop

Beitrag von ZeHa »

Also sorry AND51, aber Deine erste GameLoop funktioniert schonmal NICHT bzw. geht genau an dem vorbei, wovon ich geredet habe. Sie rendert alle 16 ms und führt die Updates so oft durch, wie es geht. GENAU DAS ist es, was ich mit meiner GameLoop NICHT haben wollte. Sondern ich will, daß die Updates alle 16 ms passieren, und daß das Rendering im IDEALFALL auch alle 16 ms passiert, und wenn die Maschine das nicht hergibt, dann halt seltener - aber das mit den Updates ist gewährleistet.

Wenn Du die Updates so oft ausführst wie möglich, dann mußt Du ja wieder die Zeit messen um dann z.B. zu schauen, wie weit sich eine Figur jetzt bewegt hat. Genau das will ich NICHT. Ich möchte, daß ich ganz simpel meine Figuren um eine feste Anzahl Pixel bewegen kann pro "Durchlauf", z.B. der eine Gegner immer 1 Pixel, die Spielfigur 2 Pixel, und irgendwas anderes vielleicht sogar 1,5 Pixel. Dadurch, daß ich nun die absolute Sicherheit habe, daß die Updates in 16 ms Schritten laufen (und notfalls nachgeholt werden, wenn das Rendering länger als 16 ms braucht), kann ich genau so programmieren und mich zurücklehnen.

Jetzt nochmal zu Deinen Threads:
Was Du meiner Meinung nach nicht ganz verstanden hast, ist, daß das Rendering auf die Daten zugreifen muß, die die Update-Funktion verändert. Das Updaten aktualisiert den gesamten Spielstand, d.h. alle Gegner, alle Figuren, die Spielfigur, alle beweglichen Wände und kA was noch alles werden verschoben, aktualisiert, entfernt, etc, je nachdem was im Spiel halt gerade abgeht. Erst wenn das ALLES erledigt ist, kann der Renderer das Bild auf den Schirm bringen. Währenddessen muß er also die Daten LESEN. Solange das passiert, hat die Update-Funktion gefälligst Pause zu machen, denn wenn die schon wieder die Positionen anpaßt, während der Renderer noch rendert, sorgt das auf dem Bild nur für Chaos. Wenn ich das nun irrsinnigerweise multithreaded mache, dann muß der Render-Thread warten, bis der Update-Thread fertig ist, und der Update-Thread wiederum muß anschließend warten, bis der Render-Thread fertig ist. Wenn man das, wie Du beschreibst, mit Mutexen macht, dann hast Du am Ende GENAU DAS GLEICHE, wie wenn Du das sequentiell machst. Der eine wartet auf den anderen.
Wenn Du die nun aber unbedingt parallel laufen lassen möchtest, dann kannst Du natürlich auch 2 Buffer vewenden, so wie von 7x7 vorgeschlagen, und die Daten jedesmal hin- und herkopieren, damit der andere dann gleich in "seinen" Daten weiterpfuschen kann. Klar, das geht auch. Aber was soll das bringen? Der Render-Thread greift die Daten sowieso wieder nur dann ab, wenn er Zeit für das nächste Bild hat. Und da er sie jetzt auch noch extra kopieren muß, bevor er loslegen kann, dauert das ganze letztendlich sogar noch länger. Trotz Multithreading.

Komplexere KI-Sachen kann man wie gesagt in Threads auslagern, diese passieren aber wirklich parallel zum Spielgeschehen. Das habe ich aber auch schon weiter vorne irgendwo geschrieben. Das hat dann auch keine unmittelbare Auswirkung sowohl auf den Spielstand als auch auf das Bild. Das heißt, Du kannst nebenher Deine KI werkeln lassen, und erst wenn sie 'ne konkrete Entscheidung getroffen hat, sorgst Du dafür, daß dieser Plan nun wieder Stück für Stück vom Updater ausgeführt wird.
ZeHa, mir scheint, als hättest du einen Verständnisfehler. Mit Threads ist man möglicherweise gleich schnell oder schneller als Singlethreaded, aber nie langsamer!
Ich weiß ganz genau, was Threads machen. Ich sage nur, daß sie in einigen Fällen sinnlos sind. Und warum sie gleich schnell oder langsamer sein können, habe ich ja jetzt oben erklärt.
Die render() Routine hat ein Delay(16) in der Schleife. So erreicht render() eine konstante Framerate!
Achso, Dein Renderer braucht null Millisekunden? Kannst Du mir den Code mal geben, sowas wollte ich schon immer mal haben
Das InitSprite() steckt ja in render()
:?
Man könnte bei Programmstart ein Intro-Video anzeigen und im Hintergrund läuft ein Thread der schon mal die Sprites lädt.
Das ist etwas völlig anderes und wurde von mir nie bestritten. Wie gesagt, ich weiß sehr wohl, was ein Thread ist und daß es sinnvoll sein kann, welche einzusetzen. Es geht in diesem Thread aber um die GameLoop! Das ist das, was die Schauspieler auf der Theaterbühne machen. Wer die Einladungen verschickt oder die Kostüme näht und wann die Schauspieler sie anziehen müssen, das spielt hierbei grad überhaupt keine Rolle.
Da werden einem schon raffinierte Methoden einfallen.
Wozu eigentlich? Ist doch überhaupt nicht notwendig. Es handelt sich um einen sequentiellen Vorgang, und ihr wollt da mit Biegen und Brechen Threads reinbauen, nur weil ihr glaubt, das wäre "besser" oder "moderner". Interessanterweise habt ihr beide (AND51 und 7x7) eure Postings begonnen mit "ich programmiere keine Spiele, ABER". Ich selber programmiere schon seit Jahren Spiele und weiß, daß da manche Dinge einfach nicht so funktionieren, wie man sich das vielleicht naiverweise vorstellen mag.
Mich erinnert das ganze grad an eine Diskussion mit xaby, als er von Isometrie-Engines geredet hat. Ich hatte schon ein paar Anläufe gewagt und mehrfach festgestellt, daß Iso, so einfach es auch aussehen mag, seine Tücken hat und man je nach Komplexität immer wieder ins nächste Problem rennt. Das wollte er mir nicht glauben und hat versucht mich mit seiner Theorie zu überzeugen, aber ich hatte das Zeug wie gesagt schon mehrfach implementiert und optimiert und wußte, wo die Grenzen sind und daß man viel tricksen muß, um keine Grafikfehler zu bekommen. Aber in seinen theoretischen Ausführungen war das alles ganz simpel und logisch, und er konnte die Probleme überhaupt nicht nachvollziehen. Eine eigene Lösung hat er aber nie implementiert.

Ich glaube euch ja, daß es gut gemeint ist und daß ihr eure Multithreading-Lösungen für "raffiniert" und ausgefuchst haltet, und ich kanns daher auch verstehen, daß ihr mich davon überzeugen wollt, daß das die "bessere" Lösung ist, aber solang ich davon keine vernünftige Implementierung sehe, die a) vom Code her sauber ist und b) tatsächlich nennenswerte Vorteile bietet in Performance oder was auch immer, bleibe ich bei meiner sequentiellen Variante, da ich mir aufgrund meiner eigenen Erfahrungen sicher bin, daß das die vernünftigere Entscheidung ist.
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
PMV
Beiträge: 2765
Registriert: 29.08.2004 13:59
Wohnort: Baden-Württemberg

Re: Re:framerate-unabhängige GameLoop - Jetzt räum ich mal h

Beitrag von PMV »

DarkDragon hat geschrieben:@AND51:
  • Platformunabhängigkeit ist nicht mehr gegeben, da auf verschiedenen Betriebssystemen verschiedene Scheduler laufen und damit unterschiedlich schnell/langsam sind.
  • Das Problem der framerate-unabhängigen Bewegung ist damit also nicht gelöst, denn der Thread läuft auf verschiedenen Systemen unterschiedlich schnell.
  • Der Aufwand das ganze auf Serialisierbarkeit etc. zu prüfen ist übertrieben für ein kleines Spiel.
  • Deadlocks führen zum langsamsten System überhaupt.
  • (Auch ein Scheduler benötigt Zeit; da dieser jedoch sowieso ständig mit läuft, weil er im Betriebssystem steckt zählt dieses Argument erstmal nicht)
Für mich sind Threads in diesem Zusammenhang keine Alternative, weil sie schlichtweg das Problem nicht lösen und diese nur in einen anderen Thread umziehen. Wenn der Thread dann mal hängt war alles für die Katz, was?
Richtig, Threads sind aufwändig und sind einzig und alleine zur Steigerung
der Performance da, wo ein einzelner Hauptthread nicht mehr genug Power
hätte. Zudem braucht dann jeder Thread sein eigenes Timing, sprich jeder
Thread muss selber seine eigenen Zeitvariablen füllen und berechnen und
er hat auch eine eigene Zielwiederholrate.
@ZeHa: deins ist aber auch nicht das gelbe vom Ei für mich ;-) . Zeitliches Supersampling ... es gibt immer Zustände, die nicht gesehen werden können.
Dir ist schon klar, das genau das einen Computer aus macht? Nichts ist
im PC kontinuierlich. Alles ist in Rastern aufgebaut. Der Bildschirm ist in
Bildpunkten aufgeteilt. Der Monitor kann zur gleichen Zeit nur ein Bild
anzeigen. Jeder Speicherbaustein kann nur einen Zustand speichern,
nicht mehrere Gleichzeitig. Somit ist im Speicher selber zur gleichen
Zeit immer nur ein Zustand vorhanden, nicht mehr. Und auch der
Sound wird durch einzelne Töne dargestellt. Usw. ... der Mensch hat
für jedes dieser "Datenströme" aber auch eine bestimmte Reaktionszeit.
Wird diese unterschritten, nimmt er es als kontinuierlich wahr. Bildfolgen
innerhalb von 25/s und mehr sieht er als Video ohne flackern. Töne
schnell hinter einander gespielt sind Musik oder schön klingende
Soundeffekte.

All diese Datenströme können parallelisiert werden. Sound läuft immer
unabhängig vom rest ab, ein mal starten und der Rest passiert alleine
im Hintergrund. Die Logik sagt lediglich: "jetzt abspielen". Das gleiche
geht aber auch mit der Grafik, es ist nur wesentlich mehr zu tun. Es
muss sicher gestellt werden, das alle Informationen, die vom Grafik-
thread gelesen werden ... auch ohne Mutexschutz gelesen werden
dürfen. Sprich es dürfen nur atomare Variablen sein. Das heißt nur
Zahlenwerte in 32-Bit Variablen bei x86, wogegen 64-Bit nur bei x64
erlaubt ist.

Wenn nun Logik und Grafik so 100% getrennt sind und komplett
unabhängig voneinander laufen, dann muss das resultierende Bild ja total
chaotisch sein, weil garnicht mehr definiert ist, wann z.b. die Position
von Objekt A gelesen wird und wann von B. Sie können ja nun aus 2
unterschiedlichen Durchgängen sein. Das stimmt, ist aber auch so gewollt
und kein Problem. Das, was der Monitor zeigt, ist immer die Vergangen-
heit. Während der Spieler gerade das aktuelle Bild betrachtet, rechnet der
Computer bereits an dem nächsten, dem eigentlichen aktuellen Spiel-
geschehen. Das wird der Spieler aber erst später sehen, wenn es bereits
nicht mehr aktuell ist. Ein Schleifendurchlauf braucht, sagen wir 10ms.
Das heißt das Spiel läuft mit 100 FPS. Der "aktuelle Frame" ist nur zu
beginn der Schleife tastächlich aktuell. Denn während der Frame auf basis
dessen Zeitwerten berechnet wird, läuft die Zeit weiter. Braucht der Frame
also 10ms um fertig berechent zu sein, so ist das Bild, das man dann
gezeigt bekommt, 10ms alt. Kurz vorm dem nächsten aktuelleren Bild,
ist der Frame übrigends 20ms alt. Sprich der Spieler sieht bei 100FPS
immer das vergangende Geschehen von vor 10-20ms.

Ich hoffe ihr konntet mir so weit folgen. Die Tatsache macht dem Spieler
aber nichts, weil 10ms für einen Menschen nicht messbar sind. Und auch
die Position usw. der Spielobjekte unterscheiden sich innerhalb der 10ms
nur sehr geringfügig. Innerhalb einer flüssigen Bewegung gar nicht
wahrnehmbar. Jetzt zurück zum Multithreading. Wir gehen mal der
Einfachheit davon aus, das Logik und Grafik auf 100FPS laufen.Was also,
wenn nun nicht mehr fest gesetzt ist, das die Szene immer 10ms alt ist,
sondern die Position usw. der Spielobjekte zwischen 0ms-10ms alt sein
können? Innerhalb eines Kontinuierlichen Bildes ist dieser Umstand nicht
wahrnehmbar. Der Spieler wird garnicht merken, das in einem Frame mal
die Position von Objekt A 10ms alt ist, und im nächsten Frame 0ms.
Diese "Sprünge" sind zu gering für das Menschliche Auge. Es muss also
lediglich sicher gestellt sein, das die Wiederholrate der Gamelogik
entsprechend schnell ist. Dann kann die Grafik getrost in einen eigenen
Thread gesteckt werden.

Um das alles aber in die Praxis um zu setzen ist sehr viel Planen nötig.
Nur wenn das ganze ohne das gegenseitige Blocken durch Mutexe
passiert, macht es wirklich sinn. Und das dann auch um zu setzten
ist weit mehr Aufwand und birgt mehr Tücken, als einem lieb ist. :lol:

MFG PMV
alte Projekte:
TSE, CWL, Chatsystem, GameMaker, AI-Game DLL, Fileparser, usw. -.-
DarkDragon
Beiträge: 6267
Registriert: 29.08.2004 08:37
Computerausstattung: Hoffentlich bald keine mehr
Kontaktdaten:

Re: Re:framerate-unabhängige GameLoop - Jetzt räum ich mal h

Beitrag von DarkDragon »

PMV hat geschrieben:
DarkDragon hat geschrieben:@AND51:
  • Platformunabhängigkeit ist nicht mehr gegeben, da auf verschiedenen Betriebssystemen verschiedene Scheduler laufen und damit unterschiedlich schnell/langsam sind.
  • Das Problem der framerate-unabhängigen Bewegung ist damit also nicht gelöst, denn der Thread läuft auf verschiedenen Systemen unterschiedlich schnell.
  • Der Aufwand das ganze auf Serialisierbarkeit etc. zu prüfen ist übertrieben für ein kleines Spiel.
  • Deadlocks führen zum langsamsten System überhaupt.
  • (Auch ein Scheduler benötigt Zeit; da dieser jedoch sowieso ständig mit läuft, weil er im Betriebssystem steckt zählt dieses Argument erstmal nicht)
Für mich sind Threads in diesem Zusammenhang keine Alternative, weil sie schlichtweg das Problem nicht lösen und diese nur in einen anderen Thread umziehen. Wenn der Thread dann mal hängt war alles für die Katz, was?
Richtig, Threads sind aufwändig und sind einzig und alleine zur Steigerung
der Performance da, wo ein einzelner Hauptthread nicht mehr genug Power
hätte. Zudem braucht dann jeder Thread sein eigenes Timing, sprich jeder
Thread muss selber seine eigenen Zeitvariablen füllen und berechnen und
er hat auch eine eigene Zielwiederholrate.
Ich hoffe doch das war kein Sarkasmus, denn AND51 wollte wohl lediglich Threads einsetzen und control() bzw update() dort rein stecken ohne zeitliche Steuerung.
PMV hat geschrieben:
@ZeHa: deins ist aber auch nicht das gelbe vom Ei für mich ;-) . Zeitliches Supersampling ... es gibt immer Zustände, die nicht gesehen werden können.
Dir ist schon klar, das genau das einen Computer aus macht? Nichts ist
im PC kontinuierlich. Alles ist in Rastern aufgebaut.
Dir ist schon klar, dass das mir klar ist? Sowas brauchst du mir nicht zu sagen. Dennoch ist der zeitliche Aufwand bei ZeHas Methode proportional zur Zeitdifferenz und wird nicht in konstanter Zeit mit analytischen Methoden berechnet.

ZeHa würde so vorgehen: Objekt A geh 1 Meter vor, Objekt A geh 1 Meter vor, Objekt A geh 1 Meter vor, Objekt A geh 1 Meter vor, Objekt A prallt gegen Wand (deshalb nicht mehr vorgehen), Abbruch
statt wie es üblich ist: Berechne Kollision zwischen Punkt 0 Meter vor und 5 Meter vor, Objekt A geh 4 Meter vor, Abbruch


Zum weiterlesen habe ich leider keine Zeit. Zuviel Text für mich und zu viele manuelle Umbrüche ... . :roll: :lol: :wink:
Angenommen es gäbe einen Algorithmus mit imaginärer Laufzeit O(i * n), dann gilt O((i * n)^2) = O(-1 * n^2) d.h. wenn man diesen Algorithmus verschachtelt ist er fertig, bevor er angefangen hat.
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: framerate-unabhängige GameLoop

Beitrag von ZeHa »

Was also,
wenn nun nicht mehr fest gesetzt ist, das die Szene immer 10ms alt ist,
sondern die Position usw. der Spielobjekte zwischen 0ms-10ms alt sein
können? Innerhalb eines Kontinuierlichen Bildes ist dieser Umstand nicht
wahrnehmbar. Der Spieler wird garnicht merken, das in einem Frame mal
die Position von Objekt A 10ms alt ist, und im nächsten Frame 0ms.
Diese "Sprünge" sind zu gering für das Menschliche Auge.
Sorry, aber ich bin der Meinung, daß man das sehr wohl sehen kann. Bei einem Spiel mehr, beim anderen weniger, beim einen Menschen mehr, beim anderen weniger. Aber solche Effekte sind definitiv wahrnehmbar. Vielleicht auch nicht 100%ig bewußt, aber als leichte Unruhe kann man sowas schon wahrnehmen. Und es sagt ja auch keiner, daß sich Objekte immer langsam bewegen. Wenn ein Objekt pro Frame 10 Pixel weit springt, dann wird dieser Effekt umso deutlicher.
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Benutzeravatar
PMV
Beiträge: 2765
Registriert: 29.08.2004 13:59
Wohnort: Baden-Württemberg

Re: framerate-unabhängige GameLoop

Beitrag von PMV »

Sollte das wirklich so sein, kann man Grafikloop und Logik immer noch
dahingehend synchronisieren, das beide die gleiche Wiederholrate haben.
Dadurch würde immer noch alles prallel laufen, aber das Springen ist damit
unterdrückt. Durch diese "leichte" Synchronisation geht allerdings wieder
etwas Unabhängigkeit verloren, doch hält sich der dann immer noch in
grenzen.

Naja ich hätte dagegen auch noch andere Ideen, aber erst mal werd ich
das ganze überhaupt mal in den nächsten Wochen umsetzten und dann
mal schauen, wie es tatsächlich läuft. :lol:
alte Projekte:
TSE, CWL, Chatsystem, GameMaker, AI-Game DLL, Fileparser, usw. -.-
Benutzeravatar
ZeHa
Beiträge: 4760
Registriert: 15.09.2004 23:57
Wohnort: Friedrichshafen
Kontaktdaten:

Re: framerate-unabhängige GameLoop

Beitrag von ZeHa »

lol LEICHTE SYNCHRONISATION? :lol:
Also jetzt wird's dann echt absurd ;) entweder du bist synchron oder nicht. Wenn nur die Rate gleich ist, dann hast Du vielleicht sogar 'nen permanenten Verschiebungs-Effekt, ähnlich dem Screen-Tearing bei ausgeschalteter VSync, und wenn Du das vermeiden willst und dementsprechend synchronisierst, dann hast Du wieder EXAKT das sequentielle Verhalten, welches bei der Spieleprogrammierung auch von jedermann angewandt wird und meiner Meinung nach immer noch das einzig sinnvolle ist ;)
Bild     Bild

ZeHa hat bisher kein Danke erhalten.
Klicke hier, wenn Du wissen möchtest, woran ihm das vorbeigeht.
Antworten