Swing, Game Loop, FPS, Threads

Ich mache ein kleines Java Spiel mit Swing, und bin verwirrt.

Ich habe bisher immer die jpanel.repaint() Methode verwendet zum rendern (momentan sind render und gameloop der gleiche loop). Damit bekomme ich extrem viel fps. Die fps sind aber wohl nicht akkurat, weil ja das painten vom EDT (Event Dispatching Thread) gemacht wird, und der fasst zeitlich nahe beeinander liegende repaints zusammen. Das heißt, die “echten fps” sind sehr viel weniger. Habe dann paintImmediatly benutzt, was mich aber weiterhin stört ist:

  1. Angeblich muss man die paintImmediatly vom EDT aus aufrufen. Wie genau geht das? Momentan rufe ich das vom Hauptthread aus auf, und es funktioniert relativ oft (kann aber durchaus scheitern in den ersten Frames)

  2. Laut Internet werden Events nicht abgefangen, solange der EDT etwas selbst geschriebenes ausführt. Ich möchte aber da ja in Zukunft meinen render loop unterbringen. Verstehe ich nicht so ganz… falls es von Interesse ist: Alle ankommende Benutzereingaben (Events) werden gespeichert und einmal pro Game loop ausgelesen. Kommen die Events weiterhin an, wenn ich im EDT quasi pausenlos durchrendere?

  3. Mit paintImmediatly erhalte ich 2000 fps. Erscheint mir immernoch etwas viel…

Edit: Mein Gameloop:

		fpsLimit = 999999;
		int frames = 0;
		long framesStartTime = System.currentTimeMillis();
		long oldTime;
		long load = 0;
		while (true) {
			oldTime = System.currentTimeMillis();
			
			frame.panel.paintImmediately(0, 0, DrawUtil.width, DrawUtil.height);
			controls.registerActions(testPlayer);
			testPlayer.executeActions(null);
			frames++;
			
			long currentTime = System.currentTimeMillis();
			if (currentTime > framesStartTime+1000) {
				NewTestGame.load = (double)load/1000;
				load = 0;
				fps = frames;
				frames = 0;
				framesStartTime = currentTime;
			}
			if (fpsLimit > 0) {
				long duration = currentTime - oldTime;
				int targetDuration = 1000/fpsLimit;
				long diff = Math.max(0, targetDuration-duration);
				load += duration;
				//TODO sleep one for more accurate 
				Thread.sleep(diff);
			}
		}

Edit2: “load” ist nur eine fancy “Lastberechnung”
z.B. wenn die fps auf 60 gecapt sind, liegt am kern maximal 7% Last an.

Ein bißchen kommt es drauf an, was das für ein Spiel werden soll. Bei schnellen, „actiongeladenen“ Spielen würde man vielleicht auf „active rendering“ zurückgreifen (die Doku von Oracle ist hier Passive vs. Active Rendering (The Java™ Tutorials > Bonus > Full-Screen Exclusive Mode API) , eins der ersten Websuchergebnisse ist Java Games: Active Rendering - General Programming - Articles - Articles - GameDev.net und sieht ganz brauchbar aus - ich selbst muss aber zugeben, active rendering noch nie wirklich verwendet zu haben…).

Ansonsten ist die FPS „nicht so wichtig“ - und wegen des Zusammenfassens von Paint-Events, das du schon erwähnt hast (und was gelegentlich „Coalescing“ genannt wird) auch schwer zu messen.

Sowas wie

long before = System.currentTimeMillis();
long fps = 0;
while (System.currentTimeMillis() < before + 1000)
{
    panel.repaint();
    fps++;
}
System.out.println("Yay: "+fps);

macht eben keinen Sinn :wink:

Es gäbe IMHO zwei Dinge, die man (nicht nur bei Swing, sondern ganz allgemein - z.B. auch bei OpenGL) messen könnte:

  1. Die Zeit vom Betreten der paint-Methode bis zu ihrem Ende (das heißt wohl manchmal „Frame Time“)
  2. Die Zeit zwischen einem Beenden der paint-Methode und dem nächsten Beenden der paint-Methode

Letzteres kommt in Richtung „FPS“, ist aber auch mit Vorsicht zu genießen.

Ansonsten reicht bei vielen Swing-Spielen IMHO auch der passive Mechanismus: Wenn sich im Spiel etwas geändert hat, wird ein „repaint()“ aufgerufen, und der neue Zustand so bald wie möglich neu gezeichnet. Wenn man das oft genug macht, ist es „flüssig“, und alles, was über 60 FPS hinausgeht, ist ohnehin sinnlos - 30 FPS wird oft als harte Grenze für „Echtzeitspiele“ angegeben (ohne die Zahl oder den Begriff jetzt im Detail hinterfragen zu wollen ;-))

Aber nochmal: Das häng stark von der Art des Spiels ab. Wenn man (wie es üblich ist) einen „Game-Thread“ hat, dann kann es z.B. passieren, dass der Game-Thread etwas am Spielgeschehen ändert, während der EDT noch die aktuelle Spielsituation zeichnet - das OK sein, aber ggf. muss man sich auch über bestimmte Formen von synchonisation Gedanken machen (Websuche „ConcurrentModificationException“, bei Listen von Spielobjekten, die geändert werden, während der EDT drüberiteriert, um sie zu zeichnen).

Du könntest auch mal einen Blick auf http://forum.byte-welt.net/java-forum-das-java-welt-kompetenz-zentrum-wir-wissen-und-helfen-/spiele-und-multimedia-programmierung/5049-quaxlis-spiele-tutorial.html werfen - in dem Tutorial werden schon viele Fragen beantwortet. Unter anderem wird dort auch auf den Punkt eingegangen, der für ein „flüssiges“, „plausibles“ Spielerlebnis oft wichtiger ist, als die reine FPS - nämlich dass die Bewegung von animierten Objekten zwischen zwei Frames nicht konstant sein sollte, sondern abhängig von der Zeit, die zwischen den Frames verstrichen ist - egal, ob das nun 30ms sind, oder 0.3ms…
@Quaxli Du warst schon eine weile nicht mehr aktiv hier, und ich weiß nicht, ob du das liest, oder wie deine aktuelle Einstellung zu diesem Thema ist, aber ich fände es toll, wenn du die Foren-Empfehlung in dem Tutorial … räusper … überdenken räääuussspeerr… und aktualisieren würdest…

Okay danke. Es geht in der Tat um ein schnelles, physics heavy action game. Die performance könnte also durchaus noch stark sinken (die 2000 fps erhalte ich quasi komplett ohne sprites/physik auf nem sehr starken rechner), wenn die Möglichkeiten der Physik stärker genutzt werden. Ich werde wohl erstmal auf active rendering umschwenken.

Langfristig wäre es aber sinnvoller, den “game loop” vom “render/event loop” zu trennen, damit z.B. die GUI & Input immer maximal responsiv bleibt, selbst wenn die fps sinken. Das synchronisieren sollte kein Problem sein, der “render loop” muss ja nur lesend auf den state zugreifen. Außerdem Multithreading und so. Sollte ja aber mit active rendering kein Problem sein. Zu den maximalen fps: Nötig ist soviel, wie die besten Bildschirme können. Also vorsichtshalber 240. Das ist doch gerade das tolle an 2D-Spielen, man braucht sich da nicht beschränken : D

Ansonsten ist es auch für die Genauigkeit der Physikberechnung sinnvoll, hohe fps Zahlen zu haben. Und natürlich wird bei mir die Physik abhängig von der verstrichenen Zeit seit dem letzen Frame berechnet, aber man hat halt trotzdem Ungenauigkeit wegen Beschleunigung.

Hach Threads, mein Spezialgebiet, der Duft von Threads in der frühen Nacht… herrlich.

Japp, in der „Theorie“ das sieht in der Praxis anderst aus. Die Threads greifen nicht immer auf den selben Speicher zu; egal wie gut man Logik vom Rendering trennt, die Threads müssen trotzdem die Objekte untereinander synchronisieren; komplexe Variablen die miteinander in Beziehung stehen werden unsyhcronisiert beschrieben und gelesen was zu Fehlern führt; etc…
Will garnicht viel dazu schreiben, zu all den Problemen gibt es Lösungen die jedoch auch immer einen großen Trade-Off haben insbesondere in ihrer Komplexität.

Das mit dem Input verhält sich unterschiedlich. Kommt der bei Swing nicht auch vom EDT? Egal wie, er muss an den Logik-Thread kommuniziert werden mithilfe eines Streams.

Bei Spielen findet meist das Worker-Thread Konzept anwendung heutzutage.
Die Workerthreads berechnen irgendetwas und geben am Ende das Ergebnis an den Main Thread zurück der es dann weiter verarbeitet, zum Beispiel Wegfindung.
Undendlich skalierbar, kaum Multithreadingprobleme und „relativ“ (verglichen mit üblichem Multithreading) einfach zu implementieren.
Physicengines lassen sich meist relativ schwer in Multithreadingkonzepte einbinden auch wenn es einige sehr gute Angebote auf dem Gebiet gibt, genauso wie OpenGL oder DirectX (die du aber nicht verwendest) weshalb trotz allem der Mainthread ein großteil der Arbeit verrichten muss, von daher ist eine Lösung mit mehreren Hauptthreads sicherlich keine schlechte Richtung, aber einfach war Multithreading noch nie.

Okay, ich dachte mir halt: Der “Render-Thread” kümmert sich um die GUI (also z.B. so Dinge wie Minimap) sowie ums Zeichnen des Games. Dabei muss er ja theoretisch nur auf die Objektpositionen/drehung und Sprites zugreifen. Problem wären dann allerdings z.B. Debuggingausgaben, die auch Werte wie Beschleunigung oder verschiedenste Werte aus anderen Komponenten des Spiels anzeigen. Ist also wahrscheinlich wirklich den Ärger (noch) nicht wert.

Wie das mit dem Input genau ist, weiß ich nicht. Ich weiß nur, dass irgendein anderer Thread (vlt. der EDT?) meine EvenListener Actions aufruft und damit meinen Inputstate ändert, den ich dann irgendwann auslese. Wird also wohl erstmal auf einen Hauptthread hinauslaufen.

Update:
Ich habe dieses Thema erstellt bzw. nach Problemen in meinem SPiel gesucht, weil mir das Spiel etwas „unflüssig“ vorkam (dass es nich wie im Zitat vorgeschlagen an der Physik liegt hab ich schon erwähnt), und habe nun den Grund gefunden:

Texture Filtering war nicht implementiert, was dazu geführt hat, dass die Sprites in Bewegung von Pixel zu Pixel gesprungen sind, anstatt eben bilinear gefiltert zu werden, was sehr unflüssig aussah. Seit ich das implementiert habe sieht das spiel extremst BUTTERWEICH aus, weicher geht nicht : D

‚Texture Filtering‘ für normales Swing mit manuellen Malen einzelner Punkte?
oder in welchen Kontext mit welcher Art Texturen?
Sprite ist ein Begriff, aber kann man ja sicher verschieden einsetzen

wenn näher damit beschäftigt für andere vielleicht auch zu finden oder eh klar,
falls du aber Zeit hast kannst du ja einen Tick ausführlicher werden :wink:

ich setzte das Thema auch auf Gelöst, wie ich es annehme

Lösung ist von java - Drawing an image using sub-pixel level accuracy using Graphics2D - Stack Overflow

Schritt 1:
drawImage mit AffineTransfirm statt direkt mit int positionen. (aktiviert double koordinaten anstatt int und damit filtering, aber nicht bilinear sondern schlechter)

AffineTransform t = new AffineTransform();
t.translate(pos.x, pos.y);
g2d.drawImage(image, t, null);

Schritt 2:
Rendering Hints für graphics Objject auf bilinear setzen (vorher).

g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);