Stutter in Game Loop

Hi,

I’m new to Java game programming. I read a lot on how to implement a game loop, but I can’t quite get it to be smooth. I’ve look all over the forums and the net and I think I’ve implemented this correctly, but the thing still jitters or stutters. Here’s my test code. It’s a very simple game loop.

If you run this, you will see the red box somewhat stutter when moving.



import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;

public class CTest
	implements Runnable {

	static final int APP_WIDTH = 640;
	static final int APP_HEIGHT = 480;

	private GraphicsEnvironment mGraphicsEnv = null;
	private GraphicsDevice mGraphicsDev = null;
	private GraphicsConfiguration mGraphicsConf = null;
	private BufferStrategy mBufferStrategy = null;

	private JFrame mFrame = null;

	private Graphics2D mGraphics = null;

	private long mStart = 0;
	private long mEnd = 0;
	private long mDT;
	private double mSeconds;

	private long mSetFPS = (1000/60) * 1000000;

	private int mBoxX = 200;
	private int mBoxY = 200;
	private int mBoxDX = 1;
	private int mBoxDY = 1;

	CTest() {
		mGraphicsEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
		mGraphicsDev = mGraphicsEnv.getDefaultScreenDevice();
		mGraphicsConf = mGraphicsDev.getDefaultConfiguration();

		mFrame = new JFrame(mGraphicsConf);
		mFrame.setIgnoreRepaint(true);
		mFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		mFrame.setLocation(100, 100);
		mFrame.setSize(APP_WIDTH, APP_HEIGHT);
		mFrame.setFocusTraversalKeysEnabled(false);
		mFrame.setResizable(false);
		mFrame.setVisible(true);

		mFrame.createBufferStrategy(2);
		mBufferStrategy = mFrame.getBufferStrategy();

		Thread thread = new Thread(this);
		thread.start();
	}

	public void run() {
		while(true) {
			mStart = System.nanoTime();

			mBoxX += mBoxDX;
			mBoxY += mBoxDY;

			if(mBoxX > APP_WIDTH - 50)
				mBoxDX = -1;
			if(mBoxX < 0)
				mBoxDX = 1;
			if(mBoxY > APP_HEIGHT - 50)
				mBoxDY = -1;
			if(mBoxY < 0)
				mBoxDY = 1;

			mGraphics = (Graphics2D)mBufferStrategy.getDrawGraphics();
			mGraphics.clearRect(0, 0, APP_WIDTH, APP_HEIGHT);
			mGraphics.setColor(Color.RED);
			mGraphics.fillRect(mBoxX, mBoxY, 50, 50);

			mGraphics.dispose();
			mBufferStrategy.show();

			mEnd = System.nanoTime();
			mDT = mEnd - mStart;

			while(mDT < mSetFPS) {
				Thread.yield();
				mEnd = System.nanoTime();
				mDT = mEnd - mStart;
			}	
		}
	}

	public static void main(String[] args) {
		new CTest();
	}
}


Did I miss something on implementing a good active rendering game loop?

The main problem you have is that your timer loop is leaking time, causing it to sync poorly with the monitors vertical refresh.

The below code corrects this :-


import java.awt.Color;
import java.awt.DisplayMode;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;

import javax.swing.JFrame;

public class CTest
	implements Runnable {

	static final int APP_WIDTH = 640;
	static final int APP_HEIGHT = 480;

	private GraphicsEnvironment mGraphicsEnv = null;
	private GraphicsDevice mGraphicsDev = null;
	private GraphicsConfiguration mGraphicsConf = null;
	private BufferStrategy mBufferStrategy = null;

	private JFrame mFrame = null;

	private int targetFps;
	
	// changed box width to 1, so tearing is much more obvious.
	public static final int BOX_WIDTH = 1;
	public static final int BOX_HEIGHT = 300;
	
	private int mBoxX = 200;
	private int mBoxY = 200;
	private int mBoxDX = 1;
	private int mBoxDY = 1;

	CTest() {
		mGraphicsEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
		mGraphicsDev = mGraphicsEnv.getDefaultScreenDevice();
		mGraphicsConf = mGraphicsDev.getDefaultConfiguration();
		
		final int currentRefreshrate = mGraphicsDev.getDisplayMode().getRefreshRate();
		// if the refresh rate is unknown, we hope 60 is ok.
		// Alternatively, you could switch to fullscreen mode for a few second, and attempt to use the vsync lock of a
		// page flipping BufferStrategy to determine the refreshrate.
		System.out.println("Reported Refreshrate=" + currentRefreshrate);
		targetFps = (currentRefreshrate==DisplayMode.REFRESH_RATE_UNKNOWN?60:currentRefreshrate);

		mFrame = new JFrame(mGraphicsConf);
		mFrame.setIgnoreRepaint(true);
		mFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		mFrame.setLocation(100, 100);
		mFrame.setSize(APP_WIDTH, APP_HEIGHT);
		mFrame.setFocusTraversalKeysEnabled(false);
		mFrame.setResizable(false);
		mFrame.setVisible(true);

		mFrame.createBufferStrategy(2);
		mBufferStrategy = mFrame.getBufferStrategy();

		Thread thread = new Thread(this);
		thread.start();
	}

	public void run() {
		
		long startTime = System.nanoTime();
		long frameCount = 0;
		
		while(true) {

			frameCount++;

			// this isn't needed for timing
			// I just use it to gauge how much drawing can be done in the frame
			// without over-running the frameEndTime. (which would cause tearing)
			final long frameStartTime = System.nanoTime();

			// time at which this frame should end so it doesn't run over the sync-time
			final long frameEndTime = startTime+(frameCount*1000000000)/targetFps;
			
			mBoxX += mBoxDX;
			mBoxY += mBoxDY;

			// this was incorrect - it was '>', where-as it should be '>=' 
			if(mBoxX >= APP_WIDTH - BOX_WIDTH)
				mBoxDX = -1;
			if(mBoxX < 0)
				mBoxDX = 1;
			// this was incorrect - it was '>', where-as it should be '>=' 
			if(mBoxY >= APP_HEIGHT - BOX_HEIGHT)
				mBoxDY = -1;
			if(mBoxY < 0)
				mBoxDY = 1;

			Graphics2D graphics = (Graphics2D)mBufferStrategy.getDrawGraphics();

			// Vary the amount of painting work.
			// This makes the game loop more representative of how a real game would behave.
			// and better demonstrates why the bufferStrategy.show() should be placed
			// after the timing loop, not before it.			
			final long paintEndTime = frameStartTime + (long)((frameEndTime-frameStartTime)*0.8*Math.random());
			while(System.nanoTime()<paintEndTime) {
				graphics.clearRect(0, 0, APP_WIDTH, APP_HEIGHT);
				// Toolkit sync() is needed to stop the Java2D pipeline buffering these commands
				// (which can cause an uncontrollable oscillation in the number of draws.)
				// Try taking this out, and run the app. a few times.
				// You will see a very bizarre effect occurring!  
				Toolkit.getDefaultToolkit().sync();
			}
			graphics.setColor(Color.RED);
			graphics.fillRect(mBoxX, mBoxY, BOX_WIDTH, BOX_HEIGHT);

			graphics.dispose();

//			mBufferStrategy.show(); // alternate (bad) place to have the show()
			
			// timing loop - you do not want to do this when vsync is supported (in fullscreen exclusive mode)
			// as it will interfere with the synchronisation performed by the vsync.
			while(System.nanoTime()<frameEndTime) {
				Thread.yield();
			}
			// do the show() after the timing loop,
			// so the copy to the screen buffer occurs at approx. the same time each frame.
			
			// if you did it before the timing loop, the time at which the copy occurs
			// will vary according to how busy the game loop is each frame - this will cause excessive tearing.

			// To see what I mean, try moving it above the timing loop
			// The fluctuations will dramatically increase the amount of tearing.
			mBufferStrategy.show();
		}
	}

	public static void main(String[] args) {
		new CTest();
	}
}

It is worth noting that there are still problems with the above solution.

The most important of these is that there is no vsync in windowed mode. (fullscreen exclusive mode does not have this limitation)
The lack of vsync will manifest itself as periodic tearing of the rendered image. You should see this in the code i’ve posted, as a judder that gradually moves up the length of the red box/line.

Unfortunately in windowed mode there is no good solution to this without using native code to access the necessary vsync functionality.

You might think you’d be able to create a timer that was accurately synchronized with the monitor’s vertical refresh…
However, this is impossible because System.nanoTime() drifts over quite short periods of time(it’ll drift approx. 3/60th of a second every minute), and System.currentTimeMillis() is not accurate enough.

:edit:

Improved the comments in the above code, and added a chunk of code to vary the amount of drawing done each frame.
This makes this synthetic example more representative of a real game,
and better demonstrates why the bufferStrategy.show() should be after the timing loop, not before it.

Thank for the tips. =) This has given me a lot to think about on how to implement my game. Does this basically mean there is no way to create smooth animation in windowed mode games, and only full screen exclusive mode games won’t suffer from the tearing/shearing?

The only thing I saw that might provide a solution was this old article:

http://today.java.net/pub/a/today/2006/02/23/smooth-moves-solutions.html

But this required JNI that exposed a wait for vsync method.

Yep, that’s the solution, but it’s only a partial solution as it works only on Windows machines with a single display.

Another alternative (which I strongly recommend) is to not use Java2D.

Ofcourse, tearing isn’t a deal-breaker, a game can be perfectly smooth & playable without the need for v-sync.
It is afterall only a minor rendering artifact.

What else can I use besides Java2D?

A very good alternative to Java2D is Slick2D. It should get your game running as smooth as butter since it uses OpenGL.

I’d second this. It also handles input (including joystick), sound, has a particle system, and plenty of example code. It’s by kev glass, who is pretty active here on the forums. If your writing a 2D game, and don’t want to write all the boilerplate, this is a great way to go.

yeah, I like slick, another one is gtge goldenstudios.or.id

How difficult is it to go from having implemented my game graphics from Java2D to Slick?

Slick has a similar API. You won’t have to deal or know about the internals of nasty things like AWT and Swing. Should be cake.