Game Loops

This subject has been touched on in other threads, particularly in the Articles & Tutorials sections, which I’ve looked through (and based my code on in fact). From what I understood, Timer (either util or Swing) is a bad idea for animation due to their lack of reliabile updating mechanisms. The seemingly “de facto” way is to use System.nanoTime() and compute your update and render time, accounting for the difference (whether fast or slow). It’s a sound method, and I’m understanding it the more I read.

I have two classes, GameLoop and GameWindow. I didn’t bother with a Canvas because java.awt.Window also implements a bufferstrategy and decided to forego adding an extra object/class to my code. Here is the relevant source:

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.swing.SwingUtilities;

public class GameLoop {
	
	public static final long FRAME_INTERVAL = 41;	// Equivalent to 24 frames per second
	
	public static void main(String[] args) {
		
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				
				final GameWindow window = new GameWindow();
				
				Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(
						new Runnable(){
							
							@Override
							public void run() {
								
								window.update();	// Perform update
								window.repaint();
							}
						}, 0L, FRAME_INTERVAL, TimeUnit.MILLISECONDS);
			}
			
		});
	}
}
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Window;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

@SuppressWarnings("serial")
public class GameWindow extends Window {
	
	private int x = 0;
	
	GameWindow() throws HeadlessException {
		
		super(null, GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration());
		
		if(!getGraphicsConfiguration().getDevice().isFullScreenSupported()) {
			
			System.out.println("Your device does not support full screen exclusive mode");
			System.exit(0);
		}
		
		addWindowListener(new WindowListener() {

			@Override
			public void windowOpened(WindowEvent e) {}

			@Override
			public void windowClosing(WindowEvent e) {
				
				getGraphicsConfiguration().getDevice().setFullScreenWindow(null);
				dispose();
			}

			@Override
			public void windowClosed(WindowEvent e) {}

			@Override
			public void windowIconified(WindowEvent e) {}

			@Override
			public void windowDeiconified(WindowEvent e) {}

			@Override
			public void windowActivated(WindowEvent e) {
				
				requestFocus();
			}

			@Override
			public void windowDeactivated(WindowEvent e) {}
			
			
		});
		
		setBounds(getGraphicsConfiguration().getBounds());
		setLayout(null);
		setIgnoreRepaint(true);
		getGraphicsConfiguration().getDevice().setFullScreenWindow(this);	// Show full screen window
		createBufferStrategy(2);
	}
	
	@Override
	public void repaint() {
		
		Graphics g = getBufferStrategy().getDrawGraphics();
		g.clearRect(0, 0, getWidth(), getHeight());
		paint(g);
		g.dispose();
		getBufferStrategy().show();	// Display the new graphics onto the canvas
		getToolkit().sync();
	}
	
	@Override
	public void paint(Graphics g) {
		
		g.fillRect(x, 0, 20, 20);
	}
	
	public void update(){
		
		x += x < 500 ? 5 : -500; 
	}
}

This works and all it does is paint a simple rectangle that moves along the upper portion of the screen. Nothing spectacular really, just something to help me get a grasp on thing.

My questions are the following:
Is it “wrong” to use an executor in this manner?
Is the executor a reliable timing mechanism?
As the next frame only executes once the prior frame has ended (due to the single thread scheduled execution) am I correct in assuming my frame rate is variable?
If the window.update() and window.render() finish prematurely, does the scheduler perform the equivalent waiting of Thread.sleep automatically?

The purpose of this is to develop some sort of reliable skeleton framework for animated games, that I understand and that isn’t just a copy/paste of someone else’s code. Use of an Executor in his fashion may obviously not be the best practice or an extremely obtuse or preposterous way of doing things as I haven’t seen it elsewhere (and there’s likely a reason for that). Any help or critiques about this code are welcome. Thanks for your input.

Using a GUI toolkit for high-speed game timing is going to be inaccurate.

I don’t understand why people read that Game Loops tutorial, go and do something else, and then ask for help as to why it isn’t working. (No offense or anything. I realise you’ve got it working and everything. I just don’t understand why people can’t won’t follow that simple tutorial.)

For something so important to how the game runs, you should just use tried and tested methods.

As a bonus, here’s a link to my WIP tutorial series. So far I’ve only got a part on game loops, but it explains in more depth how the fixed step loop works.
https://github.com/HeroesGrave/Tutorial/blob/master/tutorial/Tutorial1.md

When people say that Thread.sleep() is not accurate, they mean in by 1-3± ticks each second. While it might be true, I don’t know any other way on how to sleep threads, do you? Running a busy loop which never sleeps is a very bad idea, as it puts huge load on CPU.

LWJGL has something like Display.sync(int) which will make sure to sleep your thread so that you have solid 60 fps.

You can do something similar with core java…

While this isn’t accurate, it almost works. Depending on the nanos you put in, you will get 57-59 fps or 61-63 fps for some reason, I tried a few values and they all seemed to yield one of those 2.

If you want super accurate timer, just use LWJGL or LibGdx. I mean, its not like you will be selling your game made in java2d… Those few fps drops/increases won’t even show to the player.

EDIT-
And one more thing. You might just want to use one of those busy loops, but keep it in mind that any game you release for the public should not run on busy loops, as they REALLY use a lot of CPU…
Maybe there are like libraries to simplify this timer thing…


public class Test {

	public static void main(String[] args) {

		final FPSCounter counter = new FPSCounter();

		new Thread() {

			public void run() {

				while (true) {
					counter.frames++;
					try {
						Thread.sleep(16);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}

		}.start();

		new Thread() {

			public void run() {

				long oldTime = System.nanoTime();

				while (true) {
					long nowTime = System.nanoTime();

					if (nowTime - oldTime >= 1000000000) {
						System.out.println("The game runs at: " + counter.frames + " FPS.");
						oldTime = nowTime;
						counter.frames = 0;
					}
				}

			}

		}.start();

	}

	private static class FPSCounter {
		public int frames;
	}

}

[quote] I just don’t understand why people can’t won’t follow that simple tutorial.
[/quote]
@HeroesGraveDev – I think it is called “thinking for oneself” and/or experimentation. A lot of programmers are really big on it, to a fault. :wink:

I usually jump in and give a vote of support for the util.timer. It’s “unreliability” reputation is due to the fact that it depends on the Operating System clock, which in Windows only updates approximately every 15.5 millis. However, one can run the clock at a higher resolution by putting a Thread.sleep(Long.MAX_VALUE) command on a separate thread. (An odd fix someone discovered, I don’t know who.) With that, it is as accurate as anything else.

In “Killer Game Programming” the author demonstrate drawbacks with the swing.Timer, but concedes that the util.Timer is just as accurate as his go-to game loop.

The OP uses a ScheduledThreadPoolExecutor in place of a util.Timer. “Java Concurrency in Practice” (page 123) talks about several advantages it has over the util.Timer, and even declares that it can be “thought of as its replacement.” I’m not sure, but I think the ScheduledThreadPoolExecutor also relies on the OS clock, and thus will have the same inaccuracy as util.Timer and Thread.sleep() on Windows. I’m having trouble finding the documentation on this and have forgotten the result from then I tested this a couple years ago. Should be easy to test with console prints of System.nanoTime(), looking at the deltas.

It’s good you stuck with a SingleThread flavor of executor. If an individual game loop takes overly long, and the ScheduledThreadPoolExecutor is configured to launch another before it finishes, one better have some very well thought out concurrency-handling in place, as well as provisions for info that might not get passed from loop to loop quickly enough. With a util.Timer, a new TimerTask won’t start until the previous is finished, so this potential problem is avoided.

[EDIT: idea occurs–if one can set up the rendering to be independent of the updating (e.g., via generating new immutables each cycle for the rendering), the potential would exist for allowing a thread overlap, as long as the updates don’t cross! But maybe if the renders overlap it would create tearing or some other problem. :stuck_out_tongue: Maybe not such a good idea. Besides, probably not going to need this since monitor updates make going faster than 60fps moot.]

I haven’t checked out Java 8 yet. There may be some new capabilities associated. I’m continually being surprised by the new treats. Just discovered that mp3 audio format is now supported! Before, we had to get a non-core library to import mp3 or ogg for compressed audio.

By the way, if one uses System.nanoTime() to determine a sleep interval and then calls Thread.sleep(), I believe the same timing inaccuracy arises, due to Thread.sleep() also relying on the OS clock. This makes some of the classic game loops just as inaccurate as a util.Timer based setup.

I’m not concerned with the Timers, and I realize that inaccuracy will occur with a GUI toolkit. I don’t think I stated anywhere that I was too concerned with hyper accuracy. I realize that can’t or isn’t readily provided by the JVM. My post is concerning the concurrency libraries. Sure there are tried and true methods (I stipulated this in my post). However, the concurrency objects provide certain convenience over managing a System nanotimer. For instance, I modded the GameLoop class as follows:

package loop.game.test;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.swing.SwingUtilities;

public class GameLoop {
	
	public static final long FRAME_INTERVAL = 41;	// Equivalent to 24 frames per second
	public static ScheduledFuture<?> future;
	public static long frameRemainingTime;
	
	public static void main(String[] args) {
		
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				
				final GameWindow window = new GameWindow();
				
				future = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
						new Runnable(){
							
							@Override
							public void run() {
								
								if(frameRemainingTime > 0) {
									window.update();	// Perform update
									window.repaint();
								}
								frameRemainingTime = FRAME_INTERVAL + future.getDelay(TimeUnit.MILLISECONDS);
							}
						}, 0L, FRAME_INTERVAL, TimeUnit.MILLISECONDS);
			}
			
		});
	}
}

The getDelay() method yields the result of computations that people do with System nano. This seems a bit more user-friendly is all. However, I have yet to test it against heavy computation.

If you want really tight control (read: probably overkill) on framerate with a Thread.sleep() solution, then just have the loop monitor how inaccurate sleep() is, and compensate for any (small) inaccuracies. This can be done to at least microsecond precision with System.nanoTime().

I think a lot of the “stigma” that the core timing mechanisms are “inaccurate” is from people reading books from 2006 or earlier when there was some reason to question them. If you’re running something newer than XP you should be fine. The only real reason I see gameloops yielding framerates other than what was intended is because the person wrote incorrect timing code.

One example is active vs. passive rendering, e.g. repaint() does not paint the screen, it tells the RepaintManager “please paint this sometime soon” so use paintImmediately() instead of repaint(), but take care of which thread you call it from.

There are other techniques like using a cyclic timer (like you are doing) but incorporating semaphores, or using some hacky alternatives to Thread.sleep() for ultra sleep accuracy. Will probably result in excessive CPU usage for that last one, but if accuracy is all you care about, I doubt there is much better.

[quote]my questions are the following:
Is it “wrong” to use an executor in this manner?
Is the executor a reliable timing mechanism?
As the next frame only executes once the prior frame has ended (due to the single thread scheduled execution) am I correct in assuming my frame rate is variable?
If the window.update() and window.render() finish prematurely, does the scheduler perform the equivalent waiting of Thread.sleep automatically?
[/quote]
Your coding of the Runnable can lose the timing calculations. These are handled for you. Thus:

            future = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
                  new Runnable(){
                     
                     @Override
                     public void run() {                        
                           window.update();   // Perform update
                           window.repaint();
                        }
                     }
                  }, 0L, FRAME_INTERVAL, TimeUnit.MILLISECONDS);

2nd Question:
AFAIK, the executor is as accurate as the OS clock, and thus no more or less reliable than Thread.sleep(), System.getMillis(), or the timing of util.Timers.

3rd Question:
No, the rate is not variable, but with a couple of exceptions. Normally, the thread should have been written to complete before the delay interval elapses, in which case the executor will wait the appropriate amount before launching the next thread, similar to Thread.sleep(FRAME_INTERVAL - elapsedMillis).

However (first exception), if the execution time runs longer than the delay interval, the frame rate can slow down. So to that extent there is variability. The second exception is that if scheduleAtFixedRate() is chosen and your runnables have been running late and then one ends early, the new runnable will be launched immediately in an attempt to catch up to the schedule.

4th Question:
Yes, the code run is basically the same (in effect) as Thread.sleep(FRAME_INTERVAL - elapsedMillis), but I don’t know what the underlying implementations are or if they are the same. Also, the amount of sleep() will vary depending on both how much time was consumed by the thread, and the type of scheduling command in use. If you finish early, in one case, the executor tries to always wait the right amount to match the delay. In the other case, the executor tries to hit the scheduled times, and thus will speed up if it can, but only if it is running behind the schedule.

util.Timer has the same two scheduling modes:
schedule(TimerTask, long, long) vs. scheduleAtFixedRate(TimerTask, long, long)
http://docs.oracle.com/javase/7/docs/api/java/util/Timer.html#schedule(java.util.TimerTask,%20long,%20long)

The executor equivalent is scheduleWithFixedDelay(Runnable, long, long, TimeUnit) and scheduleAtFixedRate(Runnable, long long, TimeUnit)
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledExecutorService.html#scheduleWithFixedDelay(java.lang.Runnable,%20long,%20long,%20java.util.concurrent.TimeUnit)

I think the doc for util.Timer’s schedule() and scheduleAtFixedRate() are the clearest in explaining the distinction that also applies to the executor that you are using.

@BurntPizza – Good to know if the newer Microsoft OS’s have a faster system clock! And I guess we can finally start assuming that XP’s are being more aggressively phased out? (I’m using Ubuntu, am unable to test and verify.)

Thank you very much for your help philfrei. I’ve added a bit of code to account for extended delays, though I’m fairly certain that if the updates/renders take too long the animation will simply hang. I’ve also separated the update and render operations into their own executor processes. Changed the animation from a cube to something a bit more complex so as to create a pseudo 3d effect with a little lighting play. Includes mouse and keyboard interactivity.

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.swing.SwingUtilities;

public class GameLoop {
	
	public static final long FRAME_INTERVAL = 41;	// (milliseconds) Equivalent to 24 frames per second
	public static final long UPDATE_INTERVAL = 21;	// (milliseconds) Equivalent to 48 Hz
	
	public static final int POOL_SIZE = 3;
	
	public static long updateRepetitions;
	public static long updateCount;
	
	public static ScheduledFuture <?> updateFuture;
	public static ScheduledFuture<?> frameFuture;
	
	public static void main(String[] args) {
		
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				
				final GameCanvas game = new GameFrame().getCanvas();
				
				final ScheduledThreadPoolExecutor updateExecutor = new ScheduledThreadPoolExecutor(POOL_SIZE);
				final ScheduledThreadPoolExecutor frameExecutor = new ScheduledThreadPoolExecutor(POOL_SIZE);
				
				updateFuture = updateExecutor.scheduleAtFixedRate(new Runnable() {
					
					@Override
					public void run() {
						
						for(long i = updateRepetitions; i < 1; i++) {
							game.update();
						}
						updateRepetitions = updateFuture.getDelay(TimeUnit.MILLISECONDS) / UPDATE_INTERVAL;
					}
				}, 0L, UPDATE_INTERVAL, TimeUnit.MILLISECONDS);
				
				frameFuture = frameExecutor.scheduleAtFixedRate(new Runnable(){
					
					private long priorFrameLength = 0;	// Elapsed time of prior frame's render sequence

					@Override
					public void run() {

						if(priorFrameLength < FRAME_INTERVAL) {
							game.render();
							priorFrameLength = -frameFuture.getDelay(TimeUnit.MILLISECONDS);
						} else {
							priorFrameLength -= FRAME_INTERVAL + frameFuture.getDelay(TimeUnit.MILLISECONDS);
						}
					}
				}, UPDATE_INTERVAL, FRAME_INTERVAL, TimeUnit.MILLISECONDS);
			}
			
		});
	}
}
import java.awt.GraphicsEnvironment;

import javax.swing.JComponent;
import javax.swing.JFrame;

@SuppressWarnings("serial")
public class GameFrame extends JFrame {

	private final GameCanvas canvas;
	
	public GameFrame() {
		
		super(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration());
		
		if(!getGraphicsConfiguration().getDevice().isFullScreenSupported()) {
			
			System.out.println("No full screen support.");
			System.exit(1);
		}
		
		if(!getGraphicsConfiguration().getBufferCapabilities().isPageFlipping()) {
			
			System.out.println("No page flipping support.");
			System.exit(1);
		}
		
		setUndecorated(true);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		getContentPane().add(canvas = new GameCanvas(getGraphicsConfiguration()));
		pack();
		getGraphicsConfiguration().getDevice().setFullScreenWindow(this);
		canvas.createBufferStrategy(2);
		
		getRootPane().setActionMap(canvas.getActionMap());
		getRootPane().setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, canvas.getInputMap());
		
		canvas.requestFocus();
	}
	
	public GameCanvas getCanvas() {
		
		return canvas;
	}
	
	@Override
	public void dispose() {
		
		getGraphicsConfiguration().getDevice().setFullScreenWindow(null);
		super.dispose();
	}
}
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.KeyStroke;

@SuppressWarnings("serial")
public class GameCanvas extends Canvas {
	
	private static final double MAX_THETA = Math.PI * 4.0 / 180.0;
	private double deltaTheta;
	private Point canvasCenter;
	private Sphere[] sphere = new Sphere[9];
	
	private ActionMap actionMap = new ActionMap();
	private InputMap inputMap = new InputMap();
	
	public GameCanvas(GraphicsConfiguration configuration) {
		
		super(configuration);
		setBounds(configuration.getBounds());
		setIgnoreRepaint(true);
		
		canvasCenter = new Point(getWidth() / 2, getHeight() / 2);
		for(int i = 0; i < sphere.length; i++) {
			
			sphere[i] = new Sphere(i * 360.0 / sphere.length * Math.PI / 180.0);
		}
		
		this.addMouseMotionListener(new MouseMotionListener() {

			@Override
			public void mouseDragged(MouseEvent e) {}

			@Override
			public void mouseMoved(MouseEvent ev) {
				
				deltaTheta = MAX_THETA * (canvasCenter.getY() - ev.getY()) / canvasCenter.getY();
			}
			
		});
	}
	
	public ActionMap getActionMap() {
		
		Action up = new AbstractAction("up") {

			@Override
			public void actionPerformed(ActionEvent ev) {
				
				canvasCenter.y -= 5;
			}
			
		};
		
		Action down = new AbstractAction("down") {
			
			@Override
			public void actionPerformed(ActionEvent ev) {
				
				canvasCenter.y += 5;
			}
		};
		
		Action left = new AbstractAction("left") {
			
			@Override
			public void actionPerformed(ActionEvent ev) {
				
				canvasCenter.x -= 5;
			}
		};
		
		Action right = new AbstractAction("right") {
			
			@Override
			public void actionPerformed(ActionEvent ev) {
				
				canvasCenter.x += 5;
			}
		};
		
		actionMap.put("up", up);
		actionMap.put("down", down);
		actionMap.put("left", left);
		actionMap.put("right", right);
		
		return actionMap;
	}
	
	public InputMap getInputMap() {
		
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "up");
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "down");
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "left");
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "right");
		
		return inputMap;
	}
	
	public void render() {
		
		Graphics2D g2d = (Graphics2D)getBufferStrategy().getDrawGraphics();
		g2d.clearRect(0, 0, getWidth(), getHeight());
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		render(g2d);
		g2d.dispose();
		getBufferStrategy().show();
		getToolkit().sync();
	}
	
	private void render(Graphics2D g2d) {
		
		for(Sphere current : sphere) {
			g2d.setPaint(current.getSpecular());
			g2d.fill(current);
		}
	}
	
	public void update() {
		
		for(Sphere current : sphere) {
			current.rotate(deltaTheta);
		}
	}
	
	@Override
	public boolean isDoubleBuffered() {
		
		return true;
	}
	
	private class Sphere extends Ellipse2D.Double {
		
		private final double diameter = 40.0;
		private double x = 0;
		private double y = 0;
		private double centerX = 0;
		private double centerY = 0;
		private double travelRadius = 180.0;
		private double theta;
		
		public Sphere(double theta) {
			
			super();
			
			this.theta = theta;
			rotate(0.0);
		}
		
		public void rotate(double deltaTheta) {
			
			theta += deltaTheta;
			
			double xcomponent = Math.cos(theta);
			double ycomponent = Math.sin(theta);
			
			centerX = xcomponent * travelRadius / 2.0 + canvasCenter.getX();
			centerY = canvasCenter.getY() - ycomponent * travelRadius;
			
			double scaledDiameter = Math.abs(xcomponent - 1.0) / 2.0;
			scaledDiameter *= diameter;
			scaledDiameter += diameter;
			
			x = centerX - scaledDiameter / 2.0;
			y = centerY - scaledDiameter / 2.0;
			
			this.setFrame(x, y, scaledDiameter, scaledDiameter);
		}
		
		public RadialGradientPaint getSpecular() {
			double scaledDiameter = Math.abs(Math.cos(theta) - 1.0) / 2.0;
			int channel = (int)(255.0 * scaledDiameter);
			scaledDiameter *= getWidth();
			scaledDiameter += getWidth();
			scaledDiameter /= 2.0;
			
			return new RadialGradientPaint(
					(float) (getX() + 0.3 * getWidth()),
					(float) (getY() + 0.3 * getHeight()),
					(float) scaledDiameter,
					new float[]{0.0f, 1.0f},
					new Color[]{new Color(channel, channel, channel), Color.BLACK});
		}
	}
}