There are a lot of different sorts of game loops you can use. In general, you can think of a game loop as having 3 different pieces:
1) Some sort of, well... loop. From timer calls to while loops to recursive function calls. (the
loop)
2) A way to delay the length of each loop iteration so you get a frame rate you like. (the
timing mechanism)
3) A way to make the game speed remain independent of the speed of the loop. (the
interpolation)
Bad LoopsLet's start with some pretty ugly ones I've seen that you definitely shouldn't do:
1 2 3 4 5 6 7 8 9
| public void gameLoop() { while(true) { doGameUpdates(); render(); for (int i = 0; i < 1000000; i++) ; } } |
OMG so bad. Don't ever do that. I've literally seen it in people's games though. Aside from maxing out your processor, that's going to be a completely varying length of time for each machine and even might have variation on your machine. Bad bad bad. You can also see that this is missing a method of interpolation, but there's no way to provide that when it doesn't measure time in any way!
1 2 3 4 5 6 7 8 9
| public void gameLoop() { while(true) { doGameUpdates(); render(); Thread.sleep(1); } } |
This is better, but still not great. Thread.sleep is not always going to be accurate, plus if you've got any other threads hogging processor it won't necessarily allow your thread to resume in time. From the java docs:
Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.
Also, this is missing interpolation. That's because this sort of loop assumes that Thread.sleep() is actually going to be 100% accurate, but we know it isn't.
Another issue that we see with both of these loops is the self-defined infinite loop that is "while(true)." We know that this loop can never ever end. It's impossible. I will absolutely advise against doing this, ever. It will only cause problems. Even though, conceptually, we don't want this loop ever to end, it's a good idea to pop a boolean in there so you at least have the option if killing the loop.
1 2 3 4 5 6 7 8 9 10 11
| private boolean isRunning;
public void gameLoop() { while(isRunning) { doGameUpdates(); render(); Thread.sleep(1); } } |
Great, now we can set isRunning to false whenever we want to stop the game loop, or pause the game, or anything like that.
Here's a crazy recursive way of doing the same loop:
1 2 3 4 5 6 7 8 9 10 11 12
| private boolean isRunning;
public void gameLoop() { doGameUpdates(); render(); Thread.sleep(1); if (isRunning) { gameLoop(); } } |
I don't know why'd you ever do that, but it's another thing I've seen so I've included it for completion's sake. Aside from being not obvious, I think this will eventually cause a stack overflow (someone correct me if I'm wrong).
Decent loops, but I still wouldn't use themI think I've seen the approach of using either java.util.Timer or javax.swing.Timer more often than any other approach (at least in amateur projects). This is nice because it saves you from having to deal with any part of the loop yourself, and it's more or less accurate. The reason for this is that neither of the two Timer classes are intended to be used as heavy-lifting tasks. java.util.Timer specifically says "Timer tasks should complete quickly. If a timer task takes excessive time to complete, it "hogs" the timer's task execution thread" in the Java docs. Even better, javax.swing.Timer is meant to be used with Swing (go figure), so it is not at all a reliable timing solution. It fires all events on the EDT (event-dispatching-thread), which is used for all Swing events. So, your action might be fired after a lot of other things, thereby resulting in a very unpredictable timing solution.
Still, avoiding the overhead of dealing with your own timing system can be nice.
java.util.Timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private java.util.Timer timer; private boolean isRunning;
public void gameLoop() { timer = new Timer(); timer.schedule(new LoopyStuff(), 0, 1000 / 60); }
private class LoopyStuff extends java.util.TimerTask { public void run() { doGameUpdates(); render();
if (!isRunning) { timer.cancel(); } } } |
I won't bother laying out javax.swing.Timer, because it is totally suck. Don't use it!
Good game loopsSo, in all those examples, we've got issues with the reliability of timers. If your game logic is only updating at 30 times per second, how do you draw anything at 60 fps? Similarly, if your fps is down to 10, how do you keep the updates at 30? Or, if your game is updating at 70 times per second one frame and 10 times per second another frame, how do you ensure that the game speed is consistent for players?
Remember that third component of a game loop that we haven't used yet? That's right, we need to use interpolation.
In terms of your loop, you have two options:
- Variable timestep
- Fixed timestep
Which one you use depends on personal preference and also what sort of things you are doing in your loops.
Variable timestep loops are great because the game will seem consistent regardless of how fast the player's computer is. allowing you to potentially cater to lots of different types of machines. They also often allow you to update the game logic with very high granularity, that can make a much better experience in certain games. Things are drawn as they change position, so there is no graphical latency whatsoever.
Fixed timestep loops are great because you know that every single timestep will take up the exact same length of time. This means you will always have consistent gameplay, regardless of how fast a machine is. This works wonderfully for math-heavy games, like physics operations, and is also my loop of choice for networked games so that you know packets are going and coming at a generally constant speed. You also can keep the game logic running at a very low rate while the frame rate can still get extremely high, albeit with one frame of latency.
You can't really use variable timestep well in physics simulations, and fixed timestep fails in situations where your game can be interrupted or on machines that are too slow to hit your fixed rate.
Here's where the interpolation comes in:
- If you are using a variable stepped loop, then you need to update the game different amounts depending on how long recent updates took. You will use a delta value to do this, which you multiply times every single value that updates based on time (think of things like velocity, position, attack rate, and the like). A delta of 1.0 means that your loop took as long as you normally expect (an "optimal" amount of time), where as a delta of < 1 means that the loop is going faster than optimal, and a delta of > 1 means that it's slower.
- If you are using a fixed timestep, then you have three options: you can either have a very high fixed update (which lowers the number of machines that can reliably run your game), you can have a very low frame rate (if the positions of your characters are only updating 20 times per second, then no matter how fast the rendering is going it's only going to render those 20 frames), or you can interpolate the most recent update to the current one over each render. That sounds confusing, but it's not hard to implement, and it gives you yummy butter smoothness!
Both obviously have some caveats you're going to need to worry about. With the former, you need to multiply every single time-based updated values by the delta. This is a pain in the butt and it's pretty easy to forget to multiply by the delta sometimes, but it's reliable and it works. In the latter, you've got to multiply all time-based rendered values by the interpolation amount. Also a pain in the butt!
Personally, I usually use a fixed timestep loop, because it's generally less to think about. My logic is almost always much more complicated than my rendering, and you can usually abstract out the interpolation so that you don't have to worry about it more than once. However, I'd use whatever makes sense to you!
Without further ado, here are implementations:
Variable timestep (credit goes to Kevin Glass on
his site, with heavy changes)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| public void gameLoop() { long lastLoopTime = System.nanoTime(); final long OPTIMAL_TIME = 1000000000 / 60; while (gameRunning) { long now = System.nanoTime(); long updateLength = now - lastLoopTime; lastLoopTime = now; double delta = updateLength / ((double)OPTIMAL_TIME);
lastFpsTime += updateLength; fps++; if (lastFpsTime >= 1000000000) { System.out.println("(FPS: "+fps+")"); lastFpsTime = 0; fps = 0; } doGameUpdates(delta); render(); try{Thread.sleep( (lastLoopTime-System.nanoTime())/1000000 + 10 )}; } }
private void doGameUpdates(double delta) { for (int i = 0; i < stuff.size(); i++) { Stuff s = stuff.get(i); s.velocity += Gravity.VELOCITY * delta; s.position += s.velocity * delta; if (s.velocity >= 1000) { s.color = Color.RED; } else { s.color = Color.BLUE; } } } |
Fixed timestep (credit goes to me, this includes an example with a ball bouncing around so that you can clearly see how interpolation works)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
| import javax.swing.*; import java.awt.*; import java.awt.event.*;
public class GameLoopTest extends JFrame implements ActionListener { private GamePanel gamePanel = new GamePanel(); private JButton startButton = new JButton("Start"); private JButton quitButton = new JButton("Quit"); private JButton pauseButton = new JButton("Pause"); private boolean running = false; private boolean paused = false; private int fps = 60; private int frameCount = 0; public GameLoopTest() { super("Fixed Timestep Game Loop Test"); Container cp = getContentPane(); cp.setLayout(new BorderLayout()); JPanel p = new JPanel(); p.setLayout(new GridLayout(1,2)); p.add(startButton); p.add(pauseButton); p.add(quitButton); cp.add(gamePanel, BorderLayout.CENTER); cp.add(p, BorderLayout.SOUTH); setSize(500, 500); startButton.addActionListener(this); quitButton.addActionListener(this); pauseButton.addActionListener(this); } public static void main(String[] args) { GameLoopTest glt = new GameLoopTest(); glt.setVisible(true); } public void actionPerformed(ActionEvent e) { Object s = e.getSource(); if (s == startButton) { running = !running; if (running) { startButton.setText("Stop"); runGameLoop(); } else { startButton.setText("Start"); } } else if (s == pauseButton) { paused = !paused; if (paused) { pauseButton.setText("Unpause"); } else { pauseButton.setText("Pause"); } } else if (s == quitButton) { System.exit(0); } } public void runGameLoop() { Thread loop = new Thread() { public void run() { gameLoop(); } }; loop.start(); } private void gameLoop() { final double GAME_HERTZ = 30.0; final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ; final int MAX_UPDATES_BEFORE_RENDER = 5; double lastUpdateTime = System.nanoTime(); double lastRenderTime = System.nanoTime(); final double TARGET_FPS = 60; final double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS; int lastSecondTime = (int) (lastUpdateTime / 1000000000); while (running) { double now = System.nanoTime(); int updateCount = 0; if (!paused) { while( now - lastUpdateTime > TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BEFORE_RENDER ) { updateGame(); lastUpdateTime += TIME_BETWEEN_UPDATES; updateCount++; } if ( now - lastUpdateTime > TIME_BETWEEN_UPDATES) { lastUpdateTime = now - TIME_BETWEEN_UPDATES; } float interpolation = Math.min(1.0f, (float) ((now - lastUpdateTime) / TIME_BETWEEN_UPDATES) ); drawGame(interpolation); lastRenderTime = now; int thisSecond = (int) (lastUpdateTime / 1000000000); if (thisSecond > lastSecondTime) { System.out.println("NEW SECOND " + thisSecond + " " + frameCount); fps = frameCount; frameCount = 0; lastSecondTime = thisSecond; } while ( now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES) { Thread.yield(); try {Thread.sleep(1);} catch(Exception e) {} now = System.nanoTime(); } } } } private void updateGame() { gamePanel.update(); } private void drawGame(float interpolation) { gamePanel.setInterpolation(interpolation); gamePanel.repaint(); } private class GamePanel extends JPanel { float interpolation; float ballX, ballY, lastBallX, lastBallY; int ballWidth, ballHeight; float ballXVel, ballYVel; float ballSpeed; int lastDrawX, lastDrawY; public GamePanel() { ballX = lastBallX = 100; ballY = lastBallY = 100; ballWidth = 25; ballHeight = 25; ballSpeed = 25; ballXVel = (float) Math.random() * ballSpeed*2 - ballSpeed; ballYVel = (float) Math.random() * ballSpeed*2 - ballSpeed; } public void setInterpolation(float interp) { interpolation = interp; } public void update() { lastBallX = ballX; lastBallY = ballY; ballX += ballXVel; ballY += ballYVel; if (ballX + ballWidth/2 >= getWidth()) { ballXVel *= -1; ballX = getWidth() - ballWidth/2; ballYVel = (float) Math.random() * ballSpeed*2 - ballSpeed; } else if (ballX - ballWidth/2 <= 0) { ballXVel *= -1; ballX = ballWidth/2; } if (ballY + ballHeight/2 >= getHeight()) { ballYVel *= -1; ballY = getHeight() - ballHeight/2; ballXVel = (float) Math.random() * ballSpeed*2 - ballSpeed; } else if (ballY - ballHeight/2 <= 0) { ballYVel *= -1; ballY = ballHeight/2; } } public void paintComponent(Graphics g) { g.setColor(getBackground()); g.fillRect(lastDrawX-1, lastDrawY-1, ballWidth+2, ballHeight+2); g.fillRect(5, 0, 75, 30); g.setColor(Color.RED); int drawX = (int) ((ballX - lastBallX) * interpolation + lastBallX - ballWidth/2); int drawY = (int) ((ballY - lastBallY) * interpolation + lastBallY - ballHeight/2); g.fillOval(drawX, drawY, ballWidth, ballHeight); lastDrawX = drawX; lastDrawY = drawY; g.setColor(Color.BLACK); g.drawString("FPS: " + fps, 5, 10); frameCount++; } } private class Ball { float x, y, lastX, lastY; int width, height; float xVelocity, yVelocity; float speed; public Ball() { width = (int) (Math.random() * 50 + 10); height = (int) (Math.random() * 50 + 10); x = (float) (Math.random() * (gamePanel.getWidth() - width) + width/2); y = (float) (Math.random() * (gamePanel.getHeight() - height) + height/2); lastX = x; lastY = y; xVelocity = (float) Math.random() * speed*2 - speed; yVelocity = (float) Math.random() * speed*2 - speed; } public void update() { lastX = x; lastY = y; x += xVelocity; y += yVelocity; if (x + width/2 >= gamePanel.getWidth()) { xVelocity *= -1; x = gamePanel.getWidth() - width/2; yVelocity = (float) Math.random() * speed*2 - speed; } else if (x - width/2 <= 0) { xVelocity *= -1; x = width/2; } if (y + height/2 >= gamePanel.getHeight()) { yVelocity *= -1; y = gamePanel.getHeight() - height/2; xVelocity = (float) Math.random() * speed*2 - speed; } else if (y - height/2 <= 0) { yVelocity *= -1; y = height/2; } } public void draw(Graphics g) { } } } |
Use whatever type of loop makes sense to you, but make it once and then reuse it everywhere! I've even seen people use both fixed timestep and variable timesteps together in the same game - do whatever makes sense!
And I think that's it. Please everyone, include questions, comments, and corrections. Much of this code has not been run, and I'm sure there will be a lot of opinions about adjustments.