[GELÖST] Framerateunabhängige Programmierung

Übersicht BlitzMax, BlitzMax NG Beginners-Corner

Neue Antwort erstellen

 

CO2

ehemals "SirMO"

Betreff: [GELÖST] Framerateunabhängige Programmierung

BeitragMi, Feb 07, 2018 18:29
Antworten mit Zitat
Benutzer-Profile anzeigen
Hallo,

nach Langem melde ich mich auch mal wieder mit einem Problem:
in mordernen Spielen ist es gang und gäbe, framerateunabhängig zu programmieren, sprich: Wenn ich statt 60 FPS nur 30 FPS einstelle, dann soll die Spiellogik trotzdem nicht nur halb so schnell funktionieren. Nun stehe ich momentan vor der Frage, wie man dies am besten umsetzen könnte, ohne dass ich an die entscheidenden Stellen immer ein Faktor mit einrechnen muss (bspw. bei der Ermittlung, welche Strecke ein Objekt auf der Map zurückgelegt hat). Meine erste Idee war es, ähnlich wie bei einer virtuellen Auflösung eine "virtuelle FPS" zu bauen. Dazu habe ich eine Bezugs-FPS gebaut, welche der maximalen FPS entspricht. Nun wird in der Frame-Methode einfach geprüft, wie oft die Spiellogik durchlaufen werden muss, wobei alle Dinge, die bspw. gemalt werden nur beim letzten Durchlauf auf dem Backbuffer gemalt werden (die anderen werden per Cls() innerhalb der Frame()-Methode gelöscht).
Hier mein bisheriger Code:
BlitzMax: [AUSKLAPPEN]
Framework BRL.GLMax2D
Import BRL.Random
Import BRL.System
Import BRL.Timer
Import BRL.BMPLoader

Graphics(1920, 1080, 32)

Const REAL_FPS:Int = 75

Local FPSLimiter:TTimer = CreateTimer(REAL_FPS)

Local test:TTest = TTest.Create(REAL_FPS)

Repeat
WaitTimer(FPSLimiter)
Cls()

test.Frame()

Flip(0)
Until KeyHit(KEY_ESCAPE)
End

Type TTest
Const BASE_FPS:Int = 100

Field realfps:Int
Field currentcnt:Double
Field rest:Double

Function Create:TTest(realfps:Int)
Local RetVal:TTest = New TTest
RetVal.realfps = realfps
Return RetVal
End Function

Method Frame()
Local correctloops:Double = (Double(TTest.BASE_FPS) / Double(Self.realfps)) + Self.rest
Local realloops:Int = Int(correctloops)

For Local loop:Int = 0 To (realloops - 1)
Self.FPSIndependentFrame()

If (loop < (realloops - 1))
Cls()
End If
Next

Self.rest = correctloops - realloops
End Method

Method FPSIndependentFrame()
Self.currentcnt = Self.currentcnt + (1.0 / Self.BASE_FPS)
DrawText("Seconds: " + currentcnt, 500, 500)
End Method
End Type


Grundsätzlich finde ich dies nicht schlecht, wie die Einrechnung eines Faktors bei jeder Berechnung, welche abhängig von der Framerate ist, jedoch ist auch diese Lösung bei Weitem nicht optimal:
- Da die Spiellogik und das Malen der Bilder (bspw.) mehrfach durchlaufen wird, bleibt die Prozessorlast gleich, es wird halt nur rukeliger angezeigt (was den ganzen Sinn hinter dem Code eigentlich zunichtemacht)
- Innerhalb meines Test-Objektes rufe ich Cls() auf, was ich persönlich nicht so schön finde. Eigentlich sollte es so sein, dass sich die Hauptschleife als einziges um die elementaren Grafikbefehle kümmert, während die Testklasse nur die Spiellogik machen sollte. Diese Argumentation geht nicht ganz auf, da innerhalb der Spiellogik z.B. Bilder gemalt werden, was auch nicht von der Hauptschleife erledigt wird.
- Ist die tatsächliche Framerate größer als die Bezugsframerate, so flackert das gesamte Bild (da die For-Schleife Frameweise gar nicht durchlaufen wird)
- Der einzurechnende Faktor ist im Grunde genommen nicht raus, der einzige Vorteil ist, dass man seine Spiellogik auf der Bezugsframerate basieren lassen könnte, womit der Faktor zwar nicht in den Formeln vorkommt, er aber trotzdem automatisch mit eingerechnet wird.

Eine mögliche Verbesserung wäre es, die Draw-Befehle zu überschreiben und mittels einer globalen Variable festzulegen, ob diese "scharf" geschaltet sind, oder nicht. Dann wird nur die Spiellogik pro Frame bei 30 FPS doppelt ausgeführt, gemalt wird jedoch nur, wenn es auch wirklich sein muss. Bevor ich mich jedoch an diese Umsetzung wage, wollte ich mal in die Runde fragen, wie ihr das löst? Gibt es einen klügeren Ansatz (davon gehe ich aus), oder seid ihr zufrieden damit, dass in die Berechnungen immer ein quasikonstanter Multiplikator mit eingebaut werden muss?
mfG, CO²

Sprachen: BlitzMax, C, C++, C#, Java
Hardware: Windows 7 Ultimate 64-Bit, AMX FX-6350 (6x3,9 GHz), 32 GB RAM, Nvidia GeForce GTX 750 Ti
  • Zuletzt bearbeitet von CO2 am Sa, Feb 17, 2018 18:41, insgesamt einmal bearbeitet

DAK

BeitragMi, Feb 07, 2018 20:06
Antworten mit Zitat
Benutzer-Profile anzeigen
Hmm, ich würde hier mehr ansetzen als nur ob der Faktor in der Berechnung steht. Das ist ja im Endeffekt belanglos, nur nervig es immer dazu schreiben zu müssen.

Zur Terminologie: Analog zu FPS gibt es hier UPS, also Updates per Second, wobei ein Update hald die Spiel-Berechnungen ohne Zeichnen sind.

Der ehere Unterschied zwischen einem Faktor und Frame Skip (was im Grunde ist, was du hier machst, wo du die Updates normal machst aber das Rendern auslässt, wenn es zu langsam wird) ist, wie sich das auf die Berechnungen auswirkt.

Der Vorteil von so einem Faktor ist, dass es wesentlich weniger Rechenzeit benötigt als Frame Skip. Pro Frame wird ein Mal alles andere berechnet und das wars. Dadurch wirkt es sich nicht arg auf FPS aus. Außerdem wird das Spiel nicht langsamer, egal wie viele Updates per Second durch kommen. Selbst wenn UPS/FPS unter 1 sinken, wird das Spiel nicht langsamer.

Bei adaptivem Frame Skip wird das Spiel wohl langsamer, wenn nämlich die Updates so lange brauchen, dass die UPS unter deine gewünschten UPS fallen. Sollte das Spiel z.B. 60 Mal pro Sekunde updaten, aber die Performance reicht nur für 30, dann wird das Spiel nur mehr halb so schnell rennen. Außerdem musst du dann aufpassen, dass du noch überhaupt Renderst, sonst friert das Bild dabei ein.

Dafür hast du bei Frame Skip den Vorteil, dass das Spiel noch immer genau gleich berechnet, wie wenn es schnell genug laufen würde.
Arbeitest du mit Interpolation (dem Faktor), dann verringert sich die Genauigkeit der Berechnungen, je nach dem wie niedrig die UPS/FPS werden, bis hin zu dem Punkt, wo das Spiel anfängt zu glitchen, weil die Updates so ungenau werden, dass z.B. Fahrzeuge keine sauberen Kurven mehr fahren, sondern durch die Interpolation aus der Kurve raus getragen werden.

@zu hohe UPS/FPS: ich würde in jedem Fall bei Frame Skip eine Obergrenze an UPS/FPS implementieren, da das Spiel sonst schneller läuft und es nichts bringt. Bei einem Faktor, der immer dazu multipliziert wird, kann man die UPS/FPS eventuell schneller laufen lassen, da dann die Berechnungen genauer werden und die FPS-Fetischisten glücklicher werden.
Gewinner der 6. und der 68. BlitzCodeCompo
 

CO2

ehemals "SirMO"

BeitragMi, Feb 07, 2018 23:36
Antworten mit Zitat
Benutzer-Profile anzeigen
Hallo, Danke für die Antwort. Verstehe ich es richtig, dass das oben gezeigte System - mit einer anderen Terminologie - tatsächlich State of the Art ist? Ich hatte einen Thread gefunden https://stackoverflow.com/ques...per-second, in dem ein ähnliches Prinzip genutzt wird, jedoch wird dort das Malen der Objekte wirklich separat gemacht (In meinem Beispiel wäre es so, dass das Malen für die "Spiellogik-Frames" (sprich pro Update per Second) deaktiviert wäre). Um ein ähnliches Prinzip wie in dem obigen Thread zu nutzen, müsste ich alle zu malenden Objekte zwischenspeichern und erst beim tatsächlichen Frame per Second alle gespeicherten Objekte malen.
Ich denke, die Lösung, das Malen zu deaktivieren hört sich aktuell nach der einfachsten an - vorausgesetzt ich habe Deine Antwort richtig verstanden.
mfG, CO²

Sprachen: BlitzMax, C, C++, C#, Java
Hardware: Windows 7 Ultimate 64-Bit, AMX FX-6350 (6x3,9 GHz), 32 GB RAM, Nvidia GeForce GTX 750 Ti

Holzchopf

Meisterpacker

BeitragDo, Feb 08, 2018 7:40
Antworten mit Zitat
Benutzer-Profile anzeigen
CO2 hat Folgendes geschrieben:
Um ein ähnliches Prinzip wie in dem obigen Thread zu nutzen, müsste ich alle zu malenden Objekte zwischenspeichern und erst beim tatsächlichen Frame per Second alle gespeicherten Objekte malen.


Ich denke, das ist State of the Art. Bei Objekt-orientierter Programmierung (oder nahe dran) ist das auch absolut kein Ding.

Vor langer langer Zeit lernte man mich das EVA-Prinzip: Eingabe, Verarbeitung, Ausgabe. Und dass man seine Hauptschleife in diese 3 Teile gliedern soll. Es gab auch schon einige Situationen, bei denen ich unschöne Effekte beobachten konnte, wenn ich mich nicht strikt daran hielt. Z.B. wenn eine Figur an ihrer alten Position gezeichnet wird aber von einem Gegner an der neuen getroffen wird, was dann sich dann doch spürbar auf das Gameplay auswirken kann.

Wie dem auch sei: Bei Spielen, bei denen es essenziell ist, dass die Updaterate überall konstant ist (bspw. Netzwerkspiele), mache ich das in etwa so (Pseudocode):
BlitzMax: [AUSKLAPPEN]
While Not AppTerminate()
' Eingabe / Verarbeitung
mx = MouseX()
my = MouseY()
md = MouseDown(MOUSE_LEFT)

For Local i:Int = 0 Until ticks
mh = MouseHit(MOUSE_LEFT) ' xHit in der Schleife abfragen, es nicht in 2 aufeinander folgenden Frames 1 ist
AllObjects.Update()
Next

' Ausgabe
Cls

AllObjects.Draw()

ticks = WaitTimer(timer)
Flip(0)
Wend


Es gibt natürlich auch die Möglichkeit, die Framezeit zu messen, nur einmal zu aktualisieren und alle Bewegungen entsprechend zu skalieren. Meistens mache ich das so. Man muss dann lediglich beachten, dass bei sehr hohen Framezeiten (bei tiefen Frameraten oder wenn das Spiel aus irgendeinem Grund kurz unterdrückt wird) sehr grosse Sprünge entstehen können, wobei plötzlich Hindernisse "übersprungen" werden, also Kollisionen nicht entdeckt werden. Um dies zu verhindern, bewegt man Objekte in Teilschritten, so dass eben keine Kollision unentdeckt bleiben kann.

CO2 hat Folgendes geschrieben:
Ich denke, die Lösung, das Malen zu deaktivieren hört sich aktuell nach der einfachsten an

Das kann durchaus sein. Kommt halt drauf an, wie weit du schon bist und wie sehr du nicht Lust hast, alles neu zu schreiben Wink
Erledige alles Schritt um Schritt - erledige alles. - Holzchopf
CC BYBinaryBorn - Yogurt ♫ (31.10.2018)
Im Kopf da knackt's und knistert's sturm - 's ist kein Gedanke, nur ein Wurm

Midimaster

BeitragDo, Feb 08, 2018 9:44
Antworten mit Zitat
Benutzer-Profile anzeigen
ich mach das zufälligerweise in einem aktuellen 2D-Projekt ziemlich ähnlich...

Beim verwenden der "Delta-Distanzen"-Methode kann es passieren, dass ein Ball der gerade noch vor dem Spieler war im nächsten Step bereits hinter ihm liegt: Eine Kollision wird nicht erkannt.

Daher verwende ich eine "Delta-Zeit". In meiner Hauptschleife wird alle 1.6msec die Position der Figuren aktualisiert. Dadurch bewegt sich der Ball immer genau 1 Pixel voran: Die Bewegungsabstände sin so klein, dass die Kollision immer sicher erkannt wird.

BlitzMax: [AUSKLAPPEN]
Global Refresh%, GameTime!=MilliSecs()
Global DeltaZeit!=1.8

Repeat
Local milli!=MilliSecs()
If GameTime<milli
GameTime=GameTime+DeltaZeit
Spieler.Move()
Ball.Move()
EndIf
If Refresh <MilliSecs()
Refresh=MilliSecs()+16

Cls
TStein.MalAlle()
Spieler.Malen()
Ball.Malen
Flip 0
EndIf
Until KeyDown(KEY_ESCAPE)

Durch Verwendung des Delta-Zeit-Wert von 1.8msec läuft mein Spiel optimal schnell. Ich kann es aber sofort etwas langsamer justieren, wenn ich 2.0msec verwende oder schneller durch 1.6msec. Und dazu muss ich nur an einer einzigen zentralen Stelle etwas ändern. Sogar "Zeitlupe" zum Testen der Spiels ist möglich, indem ich den Delta-Zeit-Wert auf Tastendruck von 1.8msec auf 16msec hochstelle.

Alles Zeichnen wird in einem eigenen Block der mit fester Refresh-Rate von 16msec aufgerufen wird erledigt.
Gewinner des BCC #53 mit "Gitarrist vs Fussballer" http://www.midimaster.de/downl...ssball.exe
 

CO2

ehemals "SirMO"

BeitragDo, Feb 08, 2018 21:11
Antworten mit Zitat
Benutzer-Profile anzeigen
@DAK: Noch stehe ich am Anfang des Projekts, lediglich die Tilemap wird bisher gemalt, alle Mapobjekte haben bisher eine OnDraw()-Methode, welche ich ursprünglich auch für die Spiellogik nutzen wollte. Dies habe ich nun derart erweitert, das die Mapobjekte eine zusätzliche OnUpdate()-Methode bekommen, welche sich ausschließlich um die Logik kümmert, während die OnDraw()-Methode nur für das Malen ist.

@Midimaster: Was ich nicht so ganz verstehe: Die 16ms, die in dem Draw-Teil immer auf MilliSecs() aufaddiert werden sind maßgeblich für die FPS verantwortlich, oder? Wenn ja, heißt das, dass ich nicht mit WaitTimer() arbeiten kann, oder übersehe ich etwas? So wird ja praktisch ein Kern voll ausgelastet.
mfG, CO²

Sprachen: BlitzMax, C, C++, C#, Java
Hardware: Windows 7 Ultimate 64-Bit, AMX FX-6350 (6x3,9 GHz), 32 GB RAM, Nvidia GeForce GTX 750 Ti

Midimaster

BeitragFr, Feb 09, 2018 10:23
Antworten mit Zitat
Benutzer-Profile anzeigen
Genau, ich verbrauche in diesem Beispiel die gesamte Performance eines Kerns. Aber ist ja heutzutage in Zeiten von 8-Kern-Prozessoren nicht mehr so wichtig, oder?

Man könnte natürlich eine dritte Sektion einführen, die dann einen DELAY zulässt, wenn die Abarbeitung beider andere Sektionen nicht nötig war.
BlitzMax: [AUSKLAPPEN]
Global Refresh%, GameTime!=MilliSecs()
Global DeltaZeit!=1.8

Repeat
Local milli!=MilliSecs()
If GameTime<milli
GameTime=GameTime+DeltaZeit
Spieler.Move()
Ball.Move()
ElseIf Refresh <MilliSecs()
Refresh=MilliSecs()+16

Cls
TStein.MalAlle()
Spieler.Malen()
Ball.Malen
Flip 0
Else
Delay 1
EndIf
Until KeyDown(KEY_ESCAPE)


In diesem Beispiel triit auf meinem Rechner der DELAY-Fall etwa 500x pro Sekunde auf.

Ich verzichte erstmals auf das klassische WaitTimer(), weil ich eben extrem feine Spieltimer haben wollte und gleichzeitig der Ball sich sehr schnell mit minimalen Pixel-Abstand voranbewegen sollte. Das ganze ist ein Code für den Nachbarsjungen, der unbedingt Spieleprogrammierung lernen möchte...
Gewinner des BCC #53 mit "Gitarrist vs Fussballer" http://www.midimaster.de/downl...ssball.exe

Holzchopf

Meisterpacker

BeitragFr, Feb 09, 2018 13:01
Antworten mit Zitat
Benutzer-Profile anzeigen
Midimaster hat Folgendes geschrieben:
Aber ist ja heutzutage in Zeiten von 8-Kern-Prozessoren nicht mehr so wichtig, oder?

Tschuldigung, aber da musst du eines Besseren belehrt werden! Ich will jetzt keinen Roman aufsetzen. Aber im Grunde geht's darum, dass moderne Prozessoren die Taktrate dynamisch anpassen; läuft ein Kern (ja ein Kern genügt) auf 100% Auslastung, wird hochgetaktet. D.h. Der Prozessor nimmt mehr Strom auf, leistet mehr, wird wärmer und muss entsprechend mit mehr Aufwand kühl gehalten werden.

Ob es Wert ist, nur für ein Spiel ein paar Watt mehr an Wärmeenergie in die Umwelt zu pusten? Das muss jeder für sich selber wissen. Aber ich kann eine solche Äusserung in der Beginners-Corner, wo sie höchstwahrscheinlich ungefragt übernommen und als Mantra angeschaut wird, nicht kommentarlos stehen lassen Wink

Darüber hinaus gibt es Leute, die Software, welche aus scheinbar unersichtlichen Gründen den Prozessor so stark auslastet, als unseriös betrachten und sie entsprechend wieder von der Platte löschen Rolling Eyes
Erledige alles Schritt um Schritt - erledige alles. - Holzchopf
CC BYBinaryBorn - Yogurt ♫ (31.10.2018)
Im Kopf da knackt's und knistert's sturm - 's ist kein Gedanke, nur ein Wurm

Lobby

BeitragFr, Feb 09, 2018 21:58
Antworten mit Zitat
Benutzer-Profile anzeigen
Von der Platte löschen bei zu viel Auslastung? Zurecht, möchte ich meinen, in Zeiten in denen überall ein Miner drin versteckt sein könnte Smile

Zum Thema, ich finde man sollte differenzieren. Es ist nicht unbedingt schlecht die Aktualisierung und das Zeichnen zu koppeln. Um "Tunneleffekte" zu vermeiden sollten die entsprechenden kritischen Stellen (etwa Physikengines) von sich aus wissen, wie genau sie das handhaben (können sie auch viel besser als du, wenn du einfach auf alles die volle Rechenpower wirst und damit quasi mit Kanonen auf Spatzen schießt). Um zu weite Sprünge generell zu vermeiden könnte man die gemessene Zeit zwischen zwei Frames auf ein Maximum (bspw. 1 Sekunde) beschränken.
TheoTown - Eine Stadtaufbausimulation für Android, iOS, Windows, Mac OS und Linux
 

CO2

ehemals "SirMO"

BeitragSa, Feb 17, 2018 18:41
Antworten mit Zitat
Benutzer-Profile anzeigen
Hallo,

entschuldigt die verspätete Antwort Embarassed

Ich habe es nun einfach so gemacht, dass der ursprüngliche Mechanismus behalten wurde (es kommt bei mir nicht so sehr darauf an, pixelgenau Kollisionen erkennen zu können).
Dies habe ich so umgesetzt: Meine Tilemapklasse, welche die entsprechenden Mapobjekte kennt, hat nun zwei Methoden: Draw und Update. Jedes Mapobjekt ist ein Objekt einer abstrakten Klasse, laut dieser müssen die Methoden OnDraw und OnUpdate umgesetzt werden. OnDraw wird pro Frame einmalig aufgerufen. OnUpdate wird immer 100 mal in der Sekunde aufgerufen (dieser Wert wird vermutlich noch verändert, aktuell klappt das aber ganz gut mit diesem Wert).
Draw kümmert sich um das Zeichen, Update um die Spiellogik.
Menü, HUD, alle anderen Sachen, die es zu malen gilt und nicht von der Spiellogik abhängen werden entsprechend auch nur pro Frame einmalig gezeichnet.

Ich bedanke mich bei allen Antwortenden Very Happy
mfG, CO²

Sprachen: BlitzMax, C, C++, C#, Java
Hardware: Windows 7 Ultimate 64-Bit, AMX FX-6350 (6x3,9 GHz), 32 GB RAM, Nvidia GeForce GTX 750 Ti

Neue Antwort erstellen


Übersicht BlitzMax, BlitzMax NG Beginners-Corner

Gehe zu:

Powered by phpBB © 2001 - 2006, phpBB Group