Game Loop
Introduction
If there is one thing that all real-time games have in common, it is the game loop. This concept dominates the way that all game mechanics are implemented, as it provides a solid foundation for building complex systems of interaciton between various game components that need to be continously updated. The game loop typically executes at 60 times a second, giving us 60 "frames" per second. Each frame is essentially one iteraiton of the game loop. Given a set of game objects that need to be updated, we first update, then render them to the screen.
Example
Let's start with a user controlled rectangle that moves around on the screen in javascript because it will simplify the implementation of a game loop. The following html will serve as our starting point. Notice the canvas element, this will be instrumental in allowing us to represent pixel by pixel graphics for our game loop.
1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE html> <html> <head> <title>Game Loop</title> </head> <body> <canvas/> <script> </script> </body> </html> |
If we want to control a character, we will need to keep track of it's position and velocity - let's create a global object to represent the player.
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 | <!DOCTYPE html> <html> <head> <title>Game Loop</title> </head> <body> <canvas/> <script> let player = { position: { x: 0, y: 0 }, velocity: { x: 42, y: 42 }, size: { x: 10, y: 10 } }; </script> </body> </html> |
Now we will need to get the drawing context for the canvas and setup the game loop as an interval of 1/60th of a second. window.setInterval has two arguments; the first is a lambda function to be executed on a fixed interval, the second is the number of milliseconds to wait between executions - since "frameDuration" is in seconds, we multiply this by 1000 to get milliseconds. The update code consists of simply adding the player's velocity multiplied by the elapsed time to it's current position. This gives the effect of movement (units in canvas pixels / second). To render the player as a rectangle, we use the fillRect function of the context, which takes four arguments: top left x coordinate, top left y coordinate, width, height.
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 | <!DOCTYPE html> <html> <head> <title>Game Loop</title> </head> <body> <canvas/> <script> let player = { position: { x: 0, y: 0 }, velocity: { x: 42, y: 42 }, size: { x: 10, y: 10 } }; let ctx = document.querySelector('canvas').getContext('2d'); let frameDuration = 0.01666; window.setInterval(() => { // Update player.position.x += player.velocity.x * frameDuration; player.position.y += player.velocity.y * frameDuration; // Render ctx.fillRect(player.position.x,player.position.y,player.size.x,player.size.y); }, frameDuration * 1000); </script> </body> </html> |
If you run the above code, you will notice that the rectangle streaks across the screen leaving a black trail. I have omitted an important detail hoping you would catch it... I'm glad you did! To fix this issue we actually need to wipe the canvas clean right before we draw the next frame. This repeated "update, clear and re-draw" idea is what gives the fluid motion that we expect.
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 | <!DOCTYPE html> <html> <head> <title>Game Loop</title> </head> <body> <canvas/> <script> let player = { position: { x: 0, y: 0 }, velocity: { x: 42, y: 42 }, size: { x: 10, y: 10 } }; let ctx = document.querySelector('canvas').getContext('2d'); let frameDuration = 0.01666; window.setInterval(() => { // Update player.position.x += player.velocity.x * frameDuration; player.position.y += player.velocity.y * frameDuration; // Render ctx.clearRect(0,0,300,150); ctx.fillRect(player.position.x,player.position.y,player.size.x,player.size.y); }, frameDuration * 1000); </script> </body> </html> |
To build on what we have so far, let's add the ability for the user to control where the rectangle goes (W A S D). To do this, we simply need to capture the keydown and keyup events that fire when the user presses and releases a key. From the event keyCode, we can determine which key was pressed W=87, S=83, A=65, D=68. With that information, adjusting the velocity is as simple as assigning a new speed; a speed constant has been added to the global player variable.
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 | <!DOCTYPE html> <html> <head> <title>Game Loop</title> </head> <body> <canvas/> <script> let player = { position: { x: 0, y: 0 }, velocity: { x: 0, y: 0 }, size: { x: 10, y: 10 }, speed: 42 }; let ctx = document.querySelector('canvas').getContext('2d'); let frameDuration = 0.01666; window.setInterval(() => { // Update player.position.x += player.velocity.x * frameDuration; player.position.y += player.velocity.y * frameDuration; // Render ctx.clearRect(0,0,300,150); ctx.fillRect(player.position.x,player.position.y,player.size.x,player.size.y); }, frameDuration * 1000); window.addEventListener('keydown', (evt) => { switch (evt.keyCode) { case 87: player.velocity.y = -player.speed; break; case 83: player.velocity.y = player.speed; break; case 65: player.velocity.x = -player.speed; break; case 68: player.velocity.x = player.speed; break; } }); window.addEventListener('keyup', (evt) => { switch (evt.keyCode) { case 87: player.velocity.y = 0; break; case 83: player.velocity.y = 0; break; case 65: player.velocity.x = 0; break; case 68: player.velocity.x = 0; break; } }); </script> </body> </html> |
And that's it, the learning loop has finished it's iteration. I encourage you to experiment with this demonstration and see what else you can do.