Spiel ruckelt trotz 50 fps

Steffencz

Mitglied
Guten Morgen,

im Rahmen meiner Ausbildung programmiere ich momentan ein kleines Netzwerk Spiel. In dem Spiel steuert man einen kleinen Panzer mit der Tastatur. Und genau hier ist auch das Problem: Die Bewegung des Panzers ruckelt trotz 50 Berechnungen pro Sekunde.

Es gibt 2 Threads die für die Bewegung zuständig sind. "MovementTask" berechnet die Bewegung 50 mal pro Sekunde, während "Paint" alle vorhandenen Objekte 50 mal pro Sekunde mittels repaint() anzeigt. Stelle ich es so ein, dass es 200-250 mal pro Sekunde berechnet wird, wird es einwandfrei angezeigt. Alles darunter empfinde ich als starkes ruckeln, obwohl der Bildschirm nur 60 Bilder pro Sekunde anzeigen kann.
Da ich die dauer des Thread.sleep() selbst berechne um möglichst genau auf 50 Berechnungen pro Sekunde zu kommen ist ein performance leak ausgeschlossen. Wenn man sich die vergangene Zeit ausgeben lässt, kommt konstant 19.9 - 20 ms raus, was ja 50 fps entspricht.

Ich hoffe ihr könnt mir bei dem Problem helfen bzw. mir sagen wieso das ruckeln auftritt. Im Folgenden noch ein paar Code Ausschnitte und eine Ausführbare .jar Datei.

Paint Methode:
Java:
	@Override
	public void paintComponent(Graphics g)
	{
		super.paintComponents(g);
		drawSprite(player, g);
	}

drawSprite()
Java:
	public void drawSprite(Sprite sprite, Graphics g)
	{
		Graphics2D g21 = getNewGraphics2D(g);
		g21.setPaint(new TexturePaint(Images.getInstance().get(sprite.getImageID()), new Rectangle2D.Double(sprite
				.getX(), sprite.getY(), sprite.getWidth(), sprite.getHeight())));
		
		AffineTransform transform = Calculate.getRotatedAffineTransform(sprite.getR(), sprite.getCenterX(),
				sprite.getCenterY());
		g21.transform(transform);
		g21.fillRect((int) sprite.getX(), (int) sprite.getY(), (int) sprite.getWidth(), (int) sprite.getHeight());
		g21.dispose();
	}

getRotatedAffineTransform():
Java:
	public static AffineTransform getRotatedAffineTransform(double theta, double anchorx, double anchory)
	{
		AffineTransform transform = new AffineTransform();
		transform.rotate(theta, (int) anchorx, (int) anchory);
		return transform;
	}

repaint Task:
Java:
	@Override
	public void run()
	{
		long last = System.nanoTime();
		while (true)
		{
			last = System.nanoTime();
			repaint();
			
			Calculate.sleep(50, last);
		}
	}

eigener Sleep:
Java:
	public static void sleep(int tps, long last)
	{
		double fullDelay = (double)1e9/tps;
		long nanoDelta = System.nanoTime() - last;
		
		double targetNanoDelay = fullDelay - nanoDelta;
		if (targetNanoDelay > 0)
		{
			long delayMillis = (long) (targetNanoDelay/1e6);
			int delayNano = (int) (targetNanoDelay-delayMillis*1e6);
			try
			{
				Thread.sleep(delayMillis, delayNano);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}


Danke schon mal für die Hilfe. Falls weitere Code Ausschnitte benötigt werden sagt bitte bescheid.

MFG Steffencz
 

Anhänge

Hallo. Das könnte passieren wenn die daten beider Threads nicht abgeglichen sind. Schmeiss update und zeichnen in den gleichen Thread dann sollte es besser laufen.
 
Habs jetzt mal getestet (die .jar) auf nem mittelwertigen Laptop an dem ich sitze, da ruckelt bei mir nichts...

Edit: Schonmal getestet wie es ohne AntiAliasing aussieht?
Schonmal die FPS und TPS ausgeben lassen bzw. direkt mit zu zeichnen?
Wenns alles nichts bringt versuch mal selbst zu zeichnen?

Java:
	public void render() {
		BufferStrategy bs = getBufferStrategy();
		if (bs == null) {
			createBufferStrategy(3);
			requestFocus();
			return;
		}

		Graphics g = bs.getDrawGraphics();
		g.fillRect(0, 0, getWidth(), getHeight()); //Bildschirm leeren

//Alles Zeichnen

		g.dispose();
		bs.show();
	}
 
Zuletzt bearbeitet:
Es ist weniger ein ruckeln als eher ein "laggen". Es sollte auch nicht an der PC Leistung liegen, die CPU Auslastung bei meinem Laptop ist unter 5% bei dem Java Prozess.

Bei mir bewegt sich das Objekt nicht gleichmäßig schnell über den Bildschirm sondern hat relativ oft kleine Hüpfer, die eben erst mit einer repaint rate von ~200-250 pro Sekunde verschwinden.
 
Mhh wie weit sind den deine x bzw y verschiebungen in einem Time-Slot? kann es sein, das das nicht ordentlich dagestelt werden kann (kleiner 1px bzw gleich eine menge px)?
 
X und Y wird intern als float gespeichert, die Bewegung erfolgt mit Berücksichtigung der vergangenen Zeit. Diese Berechnung wird 50 mal pro Sekunde ausgeführt, allerdings ist das nicht das Problem. Es läuft nur flüssig, wenn ich den Zeichenvorgang öfter wiederholen lasse, auch wenn sich das Objekt in der Zwischenzeit nicht bewegt.
 
Also ich sags mal so, bei mir läuft das Proggi flüssig. Tritt das erst nach 'ner gewissen Zeit auf?
Wenn nicht, laufen deine Threads so, als wären sie einer sind also soweit übersynchronisiert, dass die run()-Methoden hintereinander, statt nebenläufig ausgeführt werden. Deswegen wird's bei dir wohl erst flüssig, wenn du die Berechnungs-"run()" öfters ausführst als die Paint-"run()".
Eine Lösung wäre, wenn du mit 2 synchronisierten BIs arbeitest. Die Berechnungen werden in das eine BI gezeichnet während das andere angezeigt wird, danach Swap der beiden BIs. Naja, wie auch immer, Hauptsache, die beiden Threads werden irgendwann zu "Nebenläufigkeiten", wie's eigentlich sein soll.
Kann auch sein, das "<Graphics>.dispose()" an falscher Stelle (oder überhaupt) angewendet wird. In der Standard-JVM ist "dispose()" jedenfalls nie nötig, weil es eigentlich nur dazu führt, das es nicht mehr verwendet werden kann (die STD-JVM sabotiert den Kontext nur, kann aber keine Ressourcen freigeben, weil schlicht keine verwendet werden) und die JVM bei jedem Repaint einen neuen erstellen muss.
 
Zuletzt bearbeitet von einem Moderator:
Deswegen wird's bei dir wohl erst flüssig, wenn du die Berechnungs-"run()" öfters ausführst als die Paint-"run()".

Da hast du mich wohl falsch verstanden. Es läuft erst dann flüssig, wenn paint wesentlich öfter ausgeführt wird als das berechnen run. Doch auch wenn ich das repaint in die berechen run Methode packe, ruckelt es hier auf dem Arbeitsrechner weiter. Ich werde es mal zuhause testen ob es dort flüssig läuft, kann ich mir aber fast nicht vorstellen.

Wie gesagt, es ist kein richtiges "ruckeln", sondern eher mikro Ruckler. Kaum sichtbar, aber mich stören sie extrem, bei anderen Java Programmen tritt das Problem nicht auf.
 
...
Wenn nicht, laufen deine Threads so, als wären sie einer sind also soweit übersynchronisiert, dass die run()-Methoden hintereinander, statt nebenläufig ausgeführt werden.
...
Verstehe ich nicht, warum das ein problem seien sollte oder verstehe ich da nur das Problem nicht dabei. Frage da nur aus interesse nach, auch wenn es ein bisschen OT ist.
 
@Steffencz: Welchem Thread man die Schuld für dieses Verhalten anlastet ist nebenläufig 🙂oops: nebensächlich natürlich).
@TryToHelp: Die Frage könnte man damit beantworten, warum man Threads überhaupt verwendet. Wenn Threads aufeinander warten müssen, braucht man sie nicht, dann genügt einer, der alles macht. Natürlich ist's gestattet, das Threads mal auf die Freigabe einer Ressource warten dürfen, damit sie neben dem anderen Thread weiter arbeiten können. Aber gerade beim Grafik berechnen und zeichnen ist die gemeinsame Ressource die Zeichenfläche, auf welche immer gewartet wird (es kann nicht gezeichnet werden, wenn noch nicht vollständig berechnet wurde), solche Threads laufen dann zwangsläufig hintereinander und nicht als Nebenläufigkeit. Die Lösung sind dann 2 Zeichenflächen, die pro Durchlauf immer ausgetauscht werden. Der Berechnungs-Thread zeichnet seine Objekte auf die eine, während die andere angezeigt wird. Selbiges geht auch mit Datenobjekten. Der Berechnungs-Thread berechnet die Daten des einen neu, während der Paint-Thread das andere zum Erstellen der Zeichenfläche verwendet. Wenn aber beide Threads das selbe Datenobjekt verwenden, kann es passieren, dass der Paint-Thread gerade jene Daten zum Zeichnen verwendet, die gerade berechnet werden.
Im übrigen ist das Ganze nicht OT, sondern zielführend. 😉
Mal als simples Beispiel:
Java:
public final class Swapping {
	private final DataObject[] data = new DataObject[] {new DataObject(), new DataObject()};
	private final Object sync = new Object();

	class Painter extends Thread {
		@Override
		public void run() {
			DataObject tmp = data[0];
			synchronized(tmp) {
				data[0] = data[1];
				data[1] = tmp;
			}
			synchronized (sync) {
				sync.notify();
			}
			tmp.repaint();
		}
	}

	class Calcer extends Thread {
		@Override
		public void run() {
			data[0].calc();
			synchronized (sync) {
				try {
					sync.wait();
				} catch(InterruptedException e) {
					
				}
			}
		}
	}
}

class DataObject {
	void calc() {
		
	}

	void repaint() {
		
	}
}
...oder ähnlich...
 
Zuletzt bearbeitet von einem Moderator:
Das würde aber eher heißen, das Problem ist eher, das sie parallel laufen und nicht das nacheinander laufen (klar ist das bei multi-threads gewünscht) somit würde dann Mutex die die einzelnen Threads (bzw die kritischen Abschnitte) haben zielführend, oder habe ich das Falsch verstanden?
 
Das würde aber eher heißen, das Problem ist eher, das sie parallel laufen und nicht das nacheinander laufen (klar ist das bei multi-threads gewünscht) somit würde dann Mutex die die einzelnen Threads (bzw die kritischen Abschnitte) haben zielführend, oder habe ich das Falsch verstanden?
Das ist richtig. Wenn man nun aber gemeinsam verwendete Ressourcen synchronisiert (was man ja eigentlich tut) laufen die Threads hintereinander.
BTW.: Jede Wette, der TO synchronisiert gar nicht, und deswegen laufen die Threads auch nebeneinander (entgegen meiner ersten Annahme) und verwursten deswegen auch manchmal gleichzeitig die selben Daten.
 
Du hast zwar recht dass sie nicht synchronisiert laufen, das hat aber unter anderem den Grund dass über das Netzwerk ja noch weitere Objekte kommen. Die Bewegung des eigenen Spielers wird Client seitig geregelt, der Rest kommt vom Server.

Nun sagst du es würde gehen wenn die Threads synchronisiert laufen würden, aber ist das nicht der Fall wenn ich von dem calculate Task aus repaint aufrufe? Dann würde immer erst dann neu gezeichnet werden, wenn die Bewegung abgeschlossen ist und es wäre synchron. Durch meine eigene sleep Methode ist auch sicher gestellt dass dieser Vorgang ziemlich regelmäßig ausgeführt wird, doch, ich habe es am Anfang getestet, das ruckeln besteht weiterhin.


Da nun viele gesagt haben bei ihnen ruckelt es nicht: Ich habe es nun zuhause probiert, ich empfinde es auch hier als ruckelnd. Es gibt auch Leute die mit 24fps spielen können und sagen das ist flüssig, ich empfinde das als unglaublich stark ruckeln. Da das Spiel noch in einem relativ frühen Stadium ist, möchte ich aber so einen "Fehler" nicht bis ans ende mitziehen sondern es möglichst schnell beheben
 
1. "repaint()" gehört nicht in den Berechnungs-Thread (jedenfalls nicht in meinem Beispiel oben). Andererseits löst "repaint()" eh' einen (Repaint)Event aus, der ohnehin schon in einem anderen Thread (EDT) ausgeführt wird. Demnach würde ein Paint-Thread eigentlich auch nur "repaint()" der Component aufrufen und evtl. die Framerate (Thread.sleep()) verzögern.
2. Was nun aber besonders wichtig ist, die "paint()"-Methode sollte eine Kopie der Objektdaten die gezeichnet werden sollen verarbeiten, statt die Objektdaten direkt, sonst kann es halt passieren, dass ein Objekt einmal unverändert in 2 Frames und beim nächsten durchlauf mit den Daten des 3. Frames gezeichnet wird und das nur, weil der Berechnungs-Thread einmal nicht schnell genug war (Zeitunterschiede liegen da manchmal nur im Nanosekundenbereich, aber das reicht völlig aus).
3. Dem Berechnungs-Thread sollte die Quelle der zur Berechnung notwendigen Daten relativ sein, solange er seinen eigenen Datencontainer bekommt. Ein weiterer Thread könnte evtl. das Netzwerk abhören und dem Berechnungs-Thread ggf. per Event- oder Observer-Model über Änderungen informieren. Berechnet wird auf jedem Fall in jenem Datenmodell (BufferedImage, DataContainer, was auch immer), welches gerade nicht zum Zeichnen verwendet wird. Andersrum: Zum Zeichnen wird genau das Datenmodell verwendet, in dem grad' keine Berechnungen stattfinden. Das Umschalten der jeweiligen Puffer geschieht entweder im Berechnungs-Thread oder in der "paint()"-Methode des Panels. jener Thread, der die Puffer nicht umschaltet (austauscht) muss pro Durchlauf nach getaner Arbeit auf das Umschalten der Puffer warten, deswegen genügt es auch, wenn man nur den "Umschalt"-Thread mittels "Thread.sleep()" in der Framerate fixiert.
4. Erklärungen für komplexe Dinge fielen mir schon immer recht schwer... evtl. solltest du dir noch mal ein paar Kapitel über MultiThreading, Concurrency und Nebenläufigkeit ansehen.
 
Ich habe mal testweise ein kleines Programm geschrieben, das einfach nur ein Viereck mit einer Koordinate (x) von links nach rechts bewegt. Selbst das ruckelt.

Ich steige auf LWJGL um, da scheint es 100% flüssig zu sein. Danke für die vielen Antworten.


MFG Steffencz
 

Zurück
Oben