No Time To Play

Game loops, input and sound in HTML5

by on May.25, 2017, under Miscellaneous

When I first started making games in what wasn’t yet known as HTML5 — not widely, anyway — about the easiest way to make a realtime game was to call a function every, say, 50 milliseconds with setInterval() and hope that would be enough time to process a frame: Javascript engines weren’t all that fast yet either. Worse, native support for video and audio was just being added to browsers, as for input, I didn’t trust myself to handle keyboard events so they would work consistently across browsers, so the mouse it was.

Needless to say, we’ve come a long way. Rhyme not intended.

In the first part of this guide, you’ve seen how to do graphics using the 2D canvas, which provides benefits of expressive power, speed, low memory and simplicity compared to the DOM. But graphics are just one aspect of games, and while other programming interfaces deal with everything in one place, HTML5 is broken up into multiple APIs you can use independently. This time, let me show you what I use to set up a game loop, accept input from the player and play sound. We’re going to focus on realtime games, because they’re conceptually simpler, but also more interesting.

To begin with, take another look at the minimal web page example from last time:

<!DOCTYPE html>
<meta charset="utf-8">
<title>HTML5 interactive example</title>
<canvas id="viewport" width="800" height="600"></canvas>
<script>
    window.addEventListener("load", function () {
        var viewport = document.getElementById("viewport");
        var context = viewport.getContext("2d");

        // Your interactive code goes here.
    });
</script>

The first thing to do is set up a game loop, and fortunately most modern browsers allow for that with just one function call:

function loop(timestamp) {
    // Process one frame.

    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

That’s it! Now your function will be called every time your monitor refreshes — almost certainly 60 times a second — unless you fail to call it again at the end of a frame. Which is exactly why you’re supposed to do that manually: so you’ll be in control. That, and because it lets you decide at which point to schedule the next one. Which begs the question, why not simply use setTimeout with a 16.7 millisecond delay? Because requestAnimationFrame does a couple of things for you:

  • automatically winds down when the game loses focus (at least it’s supposed to);
  • automatically schedules the next frame at the most appropriate time without the need for extra calculations.

The only downside is that older browsers such as IE9 don’t have it; if compatibility is a concern, there are so-called shims available online that add an implementation. My game-window.js microframework, that I used in RogueBot and Laser Sky, includes one.

For now, let’s make the newly minted game loop do something, just to see it’s working. Add these two lines at the beginning of function loop:

context.clearRect(0, 0, 800, 600);
context.fillText(Math.floor(timestamp / 1000).toString(), 0, 10);

The timestamp is in milliseconds, hence the division; requestAnimationFrame passes it to our function on every call. You can use it to tell exactly how much time has elapsed since last frame, which is important for games with physics. Well, you could also schedule the next frame before processing this one, but then the game would be in trouble on older computers.

For our purposes, however, we’ll just wing it. And the next priority is responding to input. Add the following code before the loop function:

var keys = {up: false, down: false, left: false, right: false};
window.addEventListener("keydown", function (event) {
    var key = String.fromCharCode(event.keyCode);

    switch (key) {
        case '%':
        case 'A':
            keys.left = true;
            break;
        case '(':
        case 'S':
            keys.down = true;
            break;
        case '&':
        case 'W':
            keys.up = true;
            break;
        case "'":
        case 'D':
            keys.right = true;
            break;
    }
});
window.addEventListener("keyup", function (event) {
    var key = String.fromCharCode(event.keyCode);

    switch (key) {
        case '%':
        case 'A':
            keys.left = false;
            break;
        case '(':
        case 'S':
            keys.down = false;
            break;
        case '&':
        case 'W':
            keys.up = false;
            break;
        case "'":
        case 'D':
            keys.right = false;
            break;
    }
});

I’m afraid that’s quite a bit of code, event after trimming it. What it does is map the arrow keys — with WASD as a fallback — to abstract directions (I convert event.keyCode to characters to make the code more clear). Beware that some browsers don’t emit events for special keys like the arrows, and the latter will also scroll the page when pressed; on the other hand, more casual players don’t know about WASD and will resent being asked to use those.

(As an aside, note how we leave it to the window to handle keyboard events: the HTML5 canvas can’t take focus, so it would never receive them.)

Anyway, now we can have a properly interactive game loop:

var text_x = 400;
var text_y = 300;

function loop(timestamp) {
    if (keys.left)
        text_x -= 3;
    else if (keys.right)
        text_x += 3;
    if (keys.up)
        text_y -= 3;
    else if (keys.down)
        text_y += 3;

    var seconds = Math.floor(timestamp / 1000).toString();
    context.clearRect(0, 0, 800, 600);
    context.fillText(seconds, text_x, text_y);

    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

At which point you see the reason for not changing the game state directly in the event handlers: because this way we can choose the most convenient time to get input, regardless of when the browser events fire.

This is called polling, and it’s the right way to do it in a realtime game. Which is why the HTML5 gamepad API works on the same principle outright.

Wait, did I say gamepad? Why, yes. Plenty of PC users grew up playing in arcades and on consoles as much as with a mouse and keyboard. Besides, it’s better to spread wearout across more peripherals. So in recent years web browsers have added support for controllers as well. Add the following code next to the other event handlers:

var gp0 = null;
window.addEventListener("gamepadconnected", function (event) {
    if (event.gamepad.index === 0)
        gp0 = event.gamepad;
});
window.addEventListener("gamepaddisconnected", function (event) {
    if (event.gamepad.index === 0)
        gp0 = null;
});

This works even if a gamepad is already plugged in, because then the gamepadconnected event will fire right after page load. Either way, you have to press a face button before your gamepad is detected. At last, you can poll the gamepad in your game loop like this:

if (gp0 !== null) {
    text_x += gp0.axes[0] * 5;
    text_y += gp0.axes[1] * 5;
    if (gp0.buttons[0].pressed)
        context.fillStyle = "red";
    else
        context.fillStyle = "black";
}

Here I’m assuming the gamepad has at least one pair of axes, and at least one button; you can check the length of each array to see how many you have. Too bad any D-pad will be recognized as just another pair of axes, and since all of them go from -1 to 1, there’s no way to tell them apart. Luckily the code will work all the same.

Worse, in researching for this article I learned that the above code works in Firefox but not Chrome, which requires a more complicated approach. So much for standards. Oh well, let’s file it under progressive enhancement.

The handling of mouse input is left as an exercise for the reader. For now let’s look at another piece of the puzzle: adding sound to HTML5 games.

Not that there’s much to see: the designers of this API have made it as simple as it gets. You can load and play a sound file with only two lines of code:

var snd = new Audio("mysound.ogg");
snd.play();

You might want to check first if the Audio object is available, just in case you’re dealing with an older browser. But if the sound file can’t be loaded, the code will fail quietly (no pun intended). Speaking of which: there’s no single audio format that all browsers support, to the best of my knowledge. And while there’s a way to ask the browser whether it can play a particular format, juggling with multiple encodings is a hassle. I simply never bothered, instead accepting that my games will be silent to some players.

There’s a more immediate issue: while HTML5 audio doesn’t make a distinction between effects and music, or otherwise make you juggle channels, playing multiple overlapping instances of the same sound (think the roar of many engines in a car race) is more tricky. Simply calling play again before one playback has finished will be ignored. You could keep around a couple instances of the same sound if the increased memory use doesn’t bother you, and rotate them, but I went with a simpler approach:

snd.pause();
snd.currentTime = 0;
snd.play();

Crude, but effective: before playing a sound, I always pause and rewind (there’s no stop). That’s a no-op if the sound object was already idle, and works surprisingly well in practice. Try it before more advanced solutions.

Last but not least, let’s see how to set the playback volume, because players may not want your game to be the loudest app running on their system. Sure enough, every audio object has a volume property that goes from 0 to 100. So to set the volume mid-range you’d do:

snd.volume = 50;

More could be said about making games in HTML5, for instance how to auto-pause if the window loses focus (hint: use the “blur” event to set a flag). Or how to scale the canvas with the browser — I read window.innerWidth and window.innerHeight in a “resize” handler and decide based on that. But you can get a good start just with the code I’ve shown you: roughly one hundred lines of code. The rest is all yours — your very own game. So be creative.

:, ,

2 Comments for this entry

  • Roger Kenyon

    Felix, I’ve enjoyed writing fiction with Ramus and want to push the concept further. For this I would need your help.

    Audio novels are strictly linear, of course. I would like modify Ramus so in lieu of reading and writing, there is just listening and dictating a command (choice). That is, the story would read a passage and the choices. I would say aloud one choice and the story branches to that passage.

    Apple says, with respect to CarPlay, that listening and dictating are the new reading and writing. This would be in that same spirit. In fact, one might imagine a story like I’ve describe suitable for CarPlay or at least for a bicycle trek or while trail-walking.

    Woud you be able to modify Ramus to allow for listen/speak capabilities?

    • Felix
      Felix

      Hi, Roger! It’s good to hear from you. Thank you for the nice words. I wouldn’t know where to begin, though, assuming that’s possible at all on a web page. Perhaps by leveraging accessibility features like screen reading and voice navigation?

      However, I hear there’s a tool like that from Amazon, running on the Echo. It’s been recently discussed over at the intfiction.org forums; maybe they can help?

Leave a Reply

Turn on pictures to see the captcha *

Posts by date

May 2017
M T W T F S S
« Apr   Jun »
1234567
891011121314
15161718192021
22232425262728
293031  

Posts by month