This article is a part 3 of the Let’s Write a 2D Platformer From Scratch Using HTML5 and JavaScript series.
- Part 1: Game Loop
- Part 2: Rendering
- Part 3: Collision Detection
As far as gamedev and HTML5 goes, there are tons of great game engines already out there. How do we pick the right one? Look at the number of stars on GitHub? The number of contributors? The number of published games? If you’ve looked at the previous articles on this blog, you probably know where this is heading (or read the title for that matter). We’re going to write our own game engine first!
Writing a game engine is not an easy task however. We’ll start out with just a simple 2d platformer. There won’t be any asset pipeline, and all the rendering will be done with rectangles using a simple HTML5 Canvas API. But this does not prevent us from doing animations. We’ll also write a simple tweening library to make animations and other time-based effects easy to add. But let’s first begin with the game loop.
If you’re curious to see where this series is going, here’s a little sneak peek of what we’ll have at the end of part 3, in which we implement collisions and gravity. Don’t worry if it seems like a lot of code (click on the JavaScript tag to see the source), we’ll build it up step by step in a way that everything should be clear along the way.
Click on the canvas and press A and D to move and W to jump. You can also click on the JavaScript tab to see the full code for this example.
Deploying a game like this is easy. If you click on Edit in JSFiddle on the top right, you’ll see there is a HTML and JavaScript part. The HTML only has two lines, one of which defines the canvas
, and one defines a div
used for debugging. That’s all there is needed for the game to work. After that, you can just add a script
tag with all of the code (under the JavaScript section) and you’re almost ready to go. While this blog series doesn’t rely heavily on libraries, later on we’ll add Lodash to keep the code cleaner without writing much boilerplate. Adding Lodash is either as well, all it takes is a single line to serve it from a CDN. Overall, the page to deploy the game could look something like this:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]/lodash.js"></script>
</head>
<body>
<canvas id="canvas" height="200" width="200" tabindex="1"></canvas>
<div id="debug-text"></div>
<script src="./game.js"></script>
</body>
</html>
But that’s it for the spoilers! We first need to build our game, so let’s get started.
Game loop
The core of the game loop is calculating dt
(or deltaTime
), which is the time elapsed since the last frame. Every run of the game loop then updates all of the necessary logic. If we keep dt
in seconds, we can measure all velocities as per second and calculate the per-frame update by simple multiplication. For example, if we intend to change player.x
by 40
pixels per second, we can calculate the offset of a single frame by just doing player.x += 40 * dt
. The units simply add up: px/s * s = px
.
Modern browsers have a great way of implementing the game loop with the requestAnimationFrame
function. This function takes a callback, which is then called right before the next repaint of the browser window. It only calls the callback once, which means if we want our game loop to run continually we need to call requestAnimationFrame
at the end of it. One neat feature is that the requestAnimationFrame
function calls our callback with a timestamp
argument, which basically indicates the number of milliseconds since the page has loaded.
function gameLoop(timestamp) {
// Normally we would update the game logic here.
console.log(timestamp);
// Enqueue another run of the `gameLoop` function on the next browser repaint.
window.requestAnimationFrame(gameLoop);
}
// We also initially call the `gameLoop` function via `requestAnimationFrame`.
window.requestAnimationFrame(gameLoop);
We can use the timestamp
to calculate our dt
value. The number of callbacks of requestAnimationFrame
is usually 60 times per second (60 FPS), which amounts to about 16
milliseconds per frame. We could use new Date().getTime()
to access the current time, but there is a newer and better API specifically intended for performance measurements. The function is performance.now()
and also returns the number of milliseconds since the page has loaded. This is actually the same value that window.requestAnimationFrame
passes into the callback, so we can use it to calculate the initial time before the game loop begins.
// We initialize the time of the last frame to the current time.
var lastFrame = performance.now();
// window.requestAnimationFrame calls our game loop with a timestamp
// of when the callback started being processed (in milliseconds).
function gameLoop(timestamp) {
// We calculate the time since last frame in seconds
// and update the timestamp of the last frame.
var dt = (timestamp - lastFrame) / 1000;
lastFrame = timestamp;
console.log(dt);
window.requestAnimationFrame(gameLoop);
}
window.requestAnimationFrame(gameLoop);
If we didn’t initialize the lastFrame
variable to performance.now()
, we could run into an issue of our game jumping forward in time on the first frame. This could happen especially if the game doesn’t start immediately with the page loading. To test this, try opening up the Developer Console on any web page (this one for example) and enter window.requestAnimationFrame(console.log)
after a few seconds, and you’ll see a fairly large number.
There is one problem with this approach to lastFrame
initialization. It does not work in Chrome! It works just fine in Firefox, but Chrome has an open issue since 2013 which causes it to call the gameLoop
with a timestamp lower than the initial value returned by performance.now()
. In other words, the first frame would get a negative dt
. Fortunately, the workaround isn’t terribly difficult. We can implement simple frame limiting that caps our game loop at 60 FPS, which will also fix this issue by skipping a loop if the time that has passed is less than 1000 / 60
milliseconds.
var lastFrame = performance.now();
function gameLoop(timestamp) {
// Moving `requestAnimationFrame` won't change how the loop behaves, since JavaScript
// runs synchronously from top to bottom and we can't get interrupted in the middle
// of the game loop by another call caused by an earlier `requestAnimationFrame`.
window.requestAnimationFrame(gameLoop);
// Here we simply skip the whole iteration if enough time hasn't passed yet.
if (timestamp < lastFrame + (1000 / 60)) {
return;
}
var dt = (timestamp - lastFrame) / 1000;
lastFrame = timestamp;
console.log(dt);
}
window.requestAnimationFrame(gameLoop);
One last thing we might want to do before moving on is the ability to stop the game loop. Luckily, requestAnimationFrame
returns an ID which can later be passed to window.cancelAnimationFrame()
to cancel the scheduled frame request. All we have to do is store this value in each iteration of the gameLoop
.
var lastFrame = performance.now();
var requestAnimationFrameId;
function stopGameLoop() {
window.cancelAnimationFrame(requestAnimationFrameId);
}
function gameLoop(timestamp) {
requestAnimationFrameId = window.requestAnimationFrame(gameLoop);
if (timestamp < lastFrame + (1000 / 60)) {
return;
}
var dt = (timestamp - lastFrame) / 1000;
lastFrame = timestamp;
console.log(dt);
}
requestAnimationFrameId = window.requestAnimationFrame(gameLoop);
Calculating FPS with exponential moving average
Lastly, before moving on to implement tweening I’d like to show one more useful thing. Game often have the ability to display FPS as the game is running. The easiest way is to use an exponential moving average (another resource) which requires no additional memory, compared to the often mentioned method of using an array of older values and doing a running average on those. If we used an array to store say 10
previous values, calculate the average off that on each frame, and push a new value on the next frame while popping the oldest value, we’d get what is called a moving average. The key factor there is that all values have the same weight. While in this implementation of the exponential smoothing we put more weight on newer values and decay the older ones faster and faster (exponentially). That means if we have an exponential moving average calculated from 10
values, the newer values will contribute to the result much more than the older ones.
Now let’s see how it works. First we have to pick an \(\alpha\) value which determines how quickly we decay older values. A common choice is \(\alpha = 0.1\). Then calculating the value with respect to the current frame \(FPS_{current}\) we use the value from the last calculation \(FPS_{last}\) and FPS based on the current value dt
, which is calculated as \(\frac{1}{dt}\). Putting all this together we get:
$$FPS_{current} = \alpha \cdot \frac{1}{dt} + (1 - \alpha) \cdot FPS_{last}$$
Or alternatively (after a few basic algebraic operations):
$$FPS_{current} = FPS_{last} + (1 - \alpha) \cdot (\frac{1}{dt} - FPS_{last})$$
While this might look at a lot of complicated math, it really isn’t. We’re just scaling down the old value based on \(\alpha\) as we’re adding new values. After a few iterations, the initial values was scaled down by \(\alpha\) multiple times.
Implementing this in code is easy, we just pick one of the formulas and write it as is, updating a FPS
variable after each dt
is calculated. One last note
var lastFrame = performance.now();
var requestAnimationFrameId;
var FPS = 1; // It doesn't really matter what value we initialize FPS to.
var alpha = 0.1;
function stopGameLoop() {
window.cancelAnimationFrame(requestAnimationFrameId);
}
function gameLoop(timestamp) {
requestAnimationFrameId = window.requestAnimationFrame(gameLoop);
if (timestamp < lastFrame + (1000 / 60)) {
return;
}
var dt = (timestamp - lastFrame) / 1000;
lastFrame = timestamp;
FPS = FPS + (1 - alpha) * (1/dt - FPS);
console.log(FPS);
}
requestAnimationFrameId = window.requestAnimationFrame(gameLoop);
Conclusion
This concludes the first part in this series. We’ve written a simple game loop with an FPS counter. The next article with continue with basic rendering and input handling.
This article is a part 3 of the Let’s Write a 2D Platformer From Scratch Using HTML5 and JavaScript series.
- Part 1: Game Loop
- Part 2: Rendering
- Part 3: Collision Detection
References
window.requestAnimationFrame()
- Google Chrome issue regarding
performance.now()
andwindow.requestAnimationFrame()
window.cancelAnimationFrame()
- Exponential smoothing
- Exponential moving average
Let's Write a 2D Platformer From Scratch Using HTML5 and JavaScript, Part 2: Rendering
Let's Write a 2D Platformer From Scratch Using HTML5 and JavaScript, Part 3: Collision Detection
Gamedev