An arcade joystick

Build game controls for keyboard, mouse and touch

Learn how to add keyboard, mouse and touch controls to your JavaScript game. Move a player smoothly with the arrow keys or WASD, drag objects with pointer events and build a virtual joystick for phones. By the end of this tutorial, you'll be flying your own bee through a swarm of other bees.

Part of the HTML5 game tutorial series

This tutorial builds on the game from the sprite animation part, with its game loop, swarm of animated bees and collision detection, so make sure you have that working first. Here, you take control of one of those bees. Just jumping in? Take the complete code at the end of the sprite animation part as your starting point.

Show all partsHide all parts

This tutorial starts with the keyboard, because a key is the simplest kind of input: it's either up or down. After that, you'll add mouse and touch controls, and then on-screen buttons and a virtual joystick for phones. They all work the same way, so what you learn about the keyboard carries over to the rest.

Listen for keyboard events

The browser uses two events to tell you about every key the player presses. keydown fires when a key goes down, and keyup fires when it comes back up. Add a listener for both, and the browser calls your function with an event object that tells you which key it was:

canvas.addEventListener('keydown', function (event) {
    console.log(event.code, event.key, event.repeat);
});

canvas.addEventListener('keyup', function (event) {
    console.log(event.code, event.key);
});

You might also come across a third event, keypress. It's deprecated, and it only fires for keys that type a character, not for the arrow keys or Shift. For a game, stick with keydown and keyup.

The listeners are on the canvas, not on the whole page. A canvas only gets key events when it has focus, and by default, it can't get focus at all. Give it a tabindex attribute of 0. Then the player can focus it with a click, or reach it with the Tab key in the page's normal tab order, just like a button or a text field:

<canvas id="canvas" width="750" height="400" tabindex="0"></canvas>

Only one element on the page can have focus at a time. As soon as you click somewhere else, the canvas loses focus, and your key presses go to the page instead. In the demos on this page that use the keyboard, a blue border around the canvas shows that it has focus. Click the canvas below and press some keys. Try a letter, an arrow key, Shift and the space bar, and hold one down for a while:

Live demo The code updates as you play around
// Press a key to see its event
console.log(event.code, event.key, event.repeat);

Hold down a key, and after a short pause, it starts repeating. Every repeat is a new keydown event, with event.repeat set to true. That's handy when you type text, but you'll see in a moment why it's bad news for moving things in a game.

Should you use event.key or event.code?

The demo shows two names for every key. event.key is what the key types or does, like 'w', 'W' while you hold Shift, or 'ArrowUp'. event.code is the physical key on the keyboard, like 'KeyW', no matter what it types.

That difference matters more than you'd think. Lots of games use WASD to move, because those four keys sit together under your left hand. But not every keyboard is a QWERTY keyboard. On a French AZERTY keyboard, the key where you'd normally find the W types a Z, and the A and the Q have swapped places. With event.key, a French player would have to find the W and the A somewhere else on the keyboard. event.code names every key after its place on a US keyboard, so the key that types a Z in France still gives 'KeyW'. With event.code, the four keys are in the same place for everyone.

Here's a rule of thumb:

  • event.code - For controls, where the key's position matters: moving, jumping, shooting.
  • event.key - For text, where the character matters, like a name for the high score list.

You might also come across event.keyCode, with numbers like 37 for the left arrow. It's deprecated, so don't use it in new code.

Move an object with the arrow keys

So now you know which key the player presses. In your game, the player flies one of the bees from the swarm in the sprite animation tutorial. The obvious next step is to move that bee every time an arrow key goes down. In the code below, bee is the player's bee, with its position in bee.x and bee.y:

canvas.addEventListener('keydown', function (event) {
    if (event.code == 'ArrowRight') {
        bee.x += 10;
    }
    if (event.code == 'ArrowLeft') {
        bee.x -= 10;
    }
    // ... the same for up and down
});

Give it a try. Click the canvas and hold down an arrow key:

What's going wrong here? The bee takes one step, waits, and only then starts moving. That's the key repeat you read about at the start of this tutorial: the bee only moves when a keydown event comes in. So the speed of your game depends on how fast keys repeat on the player's computer, not on your game loop.

Now hold down two arrow keys at once. Only the last key you pressed repeats, so the bee can't fly diagonally.

Keep track of which keys are pressed

The solution is to split the work. The events only keep track of which keys are down. The game loop does the rest. Every frame, it calls update(), and update() checks which keys are down and moves the bee. If you haven't built a game loop yet, the tutorial about the JavaScript game loop shows you how. Keep the state of the keys in an object, with event.code as the property name:

let keys = {};

canvas.addEventListener('keydown', function (event) {
    keys[event.code] = true;
});

canvas.addEventListener('keyup', function (event) {
    keys[event.code] = false;
});

While the player holds down the left arrow key, keys.ArrowLeft is true. A repeating key sets it to true again, which changes nothing. Now update() can move the bee at a constant speed. It gets secondsPassed, the time in seconds since the last frame:

function update(secondsPassed) {
    let speed = 200; // Pixels per second

    if (keys.ArrowLeft || keys.KeyA) {
        bee.x -= speed * secondsPassed;
    }
    if (keys.ArrowRight || keys.KeyD) {
        bee.x += speed * secondsPassed;
    }
    if (keys.ArrowUp || keys.KeyW) {
        bee.y -= speed * secondsPassed;
    }
    if (keys.ArrowDown || keys.KeyS) {
        bee.y += speed * secondsPassed;
    }
}

The arrow keys and WASD both work, so the player can use either one. If secondsPassed is new to you, the tutorial about smooth canvas animation explains it. Click the canvas and fly around. The code shows what's in keys right now:

Live demo The code updates as you play around
keys = {};

Smooth as butter, and holding two keys at once works too. Your game loop now has a new step at the start: reading the input. In this code, update() does that when it looks at keys, right before it moves the bee. After that, the loop checks for collisions, clears the canvas and draws:

Keep the same speed diagonally

There's one thing left. Hold the up and right arrows together, and the bee moves 200 pixels to the right and 200 pixels up every second. The Pythagorean theorem gives you the distance along the diagonal: Math.sqrt(200 * 200 + 200 * 200), about 283 pixels per second. So the bee flies 41% faster diagonally. Players will notice, and then they'll fly diagonally all the time.

To fix it, first combine the keys into a single direction, an x and a y. Then give that direction a length of 1. The length is the distance from (0, 0) to (x, y), and you calculate it with the same theorem. This is called normalizing a vector:

let x = 0;
let y = 0;

if (keys.ArrowLeft || keys.KeyA) {
    x -= 1;
}
// ... the same for right, up and down

// Give the direction a length of 1
let length = Math.sqrt(x * x + y * y);
if (length > 0) {
    x /= length;
    y /= length;
}

bee.x += x * speed * secondsPassed;
bee.y += y * speed * secondsPassed;

On a diagonal, x and y both start at 1, so the length is the square root of 2, about 1.41. Divide both by that, and x and y become about 0.71. Now the bee flies 200 pixels per second in every direction. Fly diagonally in the demo, and switch Normalize on and off:

Normalize

The check for length > 0 is there for when no key is down. Dividing 0 by 0 gives you NaN, and your bee vanishes without a trace.

Stop the arrow keys from scrolling the page

On a web page, the arrow keys and the space bar scroll the page. That's a problem when your game is on a page that's taller than the screen, like this one. Every time the player flies down, the page scrolls away from the game.

Call preventDefault() to stop the browser from doing what it normally does with a key. Only do that for the keys your game uses:

const gameKeys = [
    'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
    'KeyW', 'KeyA', 'KeyS', 'KeyD'
];

canvas.addEventListener('keydown', function (event) {
    keys[event.code] = true;

    // Don't let the game keys scroll the page
    if (gameKeys.includes(event.code)) {
        event.preventDefault();
    }
});

Because the listener is on the canvas, this only happens while the game has focus. Click outside the canvas, and the arrow keys scroll the page again. That's exactly what you want. Never block the arrow keys for the whole page, or a visitor who just wants to read won't be able to scroll anymore.

Players also need to see when the game has focus. That's why the demos on this page that use the keyboard get a blue border when they have focus, and most of them say Click to play when they don't. The focus and blur events of the canvas tell you when that changes.

Why do keys sometimes get stuck?

Hold down an arrow key, click outside the game and let go of the key. The canvas never gets the keyup event, because it doesn't have focus anymore. So as far as your game knows, the key is still down, and the bee keeps flying. The same thing happens when the player switches to another tab or window while holding a key.

The fix is to forget all keys when the canvas loses focus. The blur event tells you when that happens:

canvas.addEventListener('blur', function () {
    keys = {};
});

Track the mouse and fingers with pointer events

Now for the mouse. And the touchscreen, and a tablet pen. You could write code for each of them, with mouse events and touch events. But there's an easier way: pointer events. They work the same for a mouse, a finger and a pen, so one piece of code handles all of them. These are the four you'll use the most:

  • pointerdown - The mouse button goes down, or a finger touches the screen.
  • pointermove - The mouse or the finger moves.
  • pointerup - The button comes back up, or the finger leaves the screen.
  • pointercancel - The browser takes over, for example because the player starts scrolling the page.

Every event has a pointerType, which is 'mouse', 'touch' or 'pen'. And every finger gets its own pointerId, so you can tell two fingers apart. Draw on the canvas below with your mouse, or with a few fingers if you're on a phone:

The pointer event gives you clientX and clientY, the position of the pointer in the browser window. Your game needs the position in its own coordinates. The two are different whenever the canvas isn't displayed at its actual size, like on a phone, where it's scaled down to fit the screen.

Recap - The tutorial about setting up an HTML5 canvas shows how to get the mouse position on a scaled canvas. A finger gives you the same clientX and clientY, so the same code works for both.

Just like with the keys, keep the position in an object, along with whether the pointer is down. The code uses the constants canvasWidth and canvasHeight from the collision detection tutorial, which hold the width and height of your game (750 by 400 pixels):

let pointer = {x: 0, y: 0, isDown: false};

function updatePointer(event) {
    // The position on the canvas, in screen pixels
    let rect = canvas.getBoundingClientRect();
    let screenX = event.clientX - rect.left;
    let screenY = event.clientY - rect.top;

    // Convert it to game coordinates
    pointer.x = screenX * canvasWidth / rect.width;
    pointer.y = screenY * canvasHeight / rect.height;
}

canvas.addEventListener('pointerdown', function (event) {
    pointer.isDown = true;
    updatePointer(event);
});

canvas.addEventListener('pointermove', updatePointer);

canvas.addEventListener('pointerup', function () {
    pointer.isDown = false;
});

canvas.addEventListener('pointercancel', function () {
    pointer.isDown = false;
});

Just like with the keyboard, the events only keep track of where the pointer is, and the game loop decides what to do with it. Our own game, Pixi Pop, works this way. On a computer you click, on a phone you tap, and the same code handles both.

Make the bee follow the mouse or your finger

Let's put the pointer to work. While the mouse button or a finger is down, the bee flies toward it. First calculate the direction from the bee to the pointer, and then normalize it, just like you did with the arrow keys:

function update(secondsPassed) {
    let speed = 200; // Pixels per second

    if (!pointer.isDown) {
        return;
    }

    let x = pointer.x - bee.x;
    let y = pointer.y - bee.y;
    let length = Math.sqrt(x * x + y * y);

    // Stop when the bee gets there
    if (length < 10) {
        return;
    }

    bee.x += x / length * speed * secondsPassed;
    bee.y += y / length * speed * secondsPassed;
}

Press and hold anywhere on the canvas:

The bee flies in a straight line, at the same speed all the way. Without the length < 10 check, the bee would never stop. Once it's less than one step away from the pointer, the next step would carry it past the pointer, and it would jitter back and forth.

Want the bee to curve toward the pointer and slow down as it gets close? The tutorial on flocking with boids shows you how to do that with steering.

Detect a click or tap on an object

Sometimes, instead of steering, the player clicks or taps something: a button, an enemy, a card. Then you need to know what's under the pointer. A click is just a point, so this is a job for collision detection. The tutorial about collision detection has a function that checks if a point is inside a circle:

function pointInCircle(px, py, cx, cy, r) {
    let dx = px - cx;
    let dy = py - cy;
    return dx * dx + dy * dy <= r * r;
}

It compares the squared distance with the squared radius. That gives the same answer as comparing the distance itself, but it saves you a Math.sqrt() call. Loop over gameObjects, the array that holds all the bees, and check each one. Go through the array backward, because the bee that's drawn last is on top. When the pointer hits a bee, call bump() to play the bump animation from the sprite animation tutorial, which makes the bee dizzy for a moment:

canvas.addEventListener('pointerdown', function (event) {
    updatePointer(event);

    for (let i = gameObjects.length - 1; i >= 0; i--) {
        let bee = gameObjects[i];
        if (pointInCircle(pointer.x, pointer.y, bee.x, bee.y, 25)) {
            bee.bump();
            break;
        }
    }
});

In the Bee class, the hitbox has a radius of 15, but this check uses 25. A fingertip is a lot bigger than a mouse pointer, and nobody likes to miss. Try it, and catch a bee to make it dizzy:

Drag and drop objects on the canvas

Now that you know which bee is under the pointer, you can do more than make it dizzy. In this section, you'll let the player grab a bee, drag it across the canvas and drop it somewhere else.

Dragging takes three steps. On pointerdown, find the bee under the pointer, the same way as above. On pointermove, move it with the pointer. And on pointerup, let it go.

Two details make the difference between dragging that feels right and dragging that feels clumsy. Remember where you grabbed the bee, so its middle doesn't jump to the pointer. And call setPointerCapture(), so the canvas keeps getting pointermove events, even when the pointer leaves the canvas:

let dragged = null;
let grabX = 0;
let grabY = 0;

canvas.addEventListener('pointerdown', function (event) {
    updatePointer(event);
    dragged = findBee(pointer.x, pointer.y);
    if (dragged) {
        // Where you grabbed the bee, relative to its middle
        grabX = pointer.x - dragged.x;
        grabY = pointer.y - dragged.y;
        canvas.setPointerCapture(event.pointerId);

        // Stop the bee so only the pointer moves it
        dragged.vx = 0;
        dragged.vy = 0;
    }
});

canvas.addEventListener('pointermove', function (event) {
    updatePointer(event);
    if (dragged) {
        dragged.x = pointer.x - grabX;
        dragged.y = pointer.y - grabY;
    }
});

canvas.addEventListener('pointerup', function () {
    dragged = null;
});

findBee() is the loop from the previous section, wrapped in a function. It returns the bee it finds, or null. Don't forget pointercancel: it should let go of the bee too.

Want to make it more fun? Keep track of how fast the pointer moves while you drag: how far it moves in x and in y, divided by how long that took. event.timeStamp tells you when each pointermove happened. When you let go, give the bee that velocity as vx and vy, and you can throw it. The collision detection you added earlier in the series does the rest. Grab a bee and throw it into the swarm:

Stop the page from scrolling on a touchscreen

Try dragging on a phone, and you'll run into a problem. The browser thinks you want to scroll the page or zoom in. It scrolls, sends a pointercancel event, and stops sending pointermove events to your game.

The CSS property touch-action tells the browser what to do with a touch. Set it to none, and the browser leaves every touch on the canvas to your game:

#canvas {
    touch-action: none;
}

For a game that fills the screen, that's all you need. But on this page, the demos sit in the middle of a lot of text. With touch-action: none, your thumb would get stuck on a demo every time you tried to scroll past it on a phone. That's why the demos here only turn it on after you tap them, and turn it off again when you tap somewhere else:

canvas.addEventListener('focus', function () {
    canvas.style.touchAction = 'none';
});

canvas.addEventListener('blur', function () {
    canvas.style.touchAction = '';
});

The demo where you drag the bees prevents scrolling in a different way. It listens for touchstart, the touch event that fires when a finger touches the screen, and calls preventDefault() only when the finger lands on a bee. It needs a touch event for this, because preventDefault() on a pointer event doesn't stop the page from scrolling. If the finger lands anywhere else, the page scrolls as usual.

Add on-screen touch buttons

A phone has no arrow keys, so why not draw them on the screen? On-screen buttons are just rectangles on the canvas. On pointerdown, check which button is under the finger, the same way you checked for a bee.

Here's the nice part. Let the buttons use the same keys object as the keyboard. Then the rest of your game won't even notice the difference:

let buttons = [
    {code: 'ArrowLeft', x: 20, y: 215, width: 70, height: 70},
    {code: 'ArrowRight', x: 170, y: 215, width: 70, height: 70},
    // ... and the buttons for up and down
];

// The button each finger is pressing
let pressed = {};

canvas.addEventListener('pointerdown', function (event) {
    updatePointer(event);
    for (let i = 0; i < buttons.length; i++) {
        let button = buttons[i];
        if (pointInRect(
            pointer.x, pointer.y,
            button.x, button.y, button.width, button.height
        )) {
            keys[button.code] = true;
            pressed[event.pointerId] = button.code;
        }
    }
});

function releaseButton(event) {
    let code = pressed[event.pointerId];
    if (code) {
        keys[code] = false;
        delete pressed[event.pointerId];
    }
}

canvas.addEventListener('pointerup', releaseButton);
canvas.addEventListener('pointercancel', releaseButton);

pointInRect(), which checks if a point is inside a rectangle, comes from the collision detection tutorial as well. Every finger has its own pointerId, so pressed knows which button to release when a finger lifts off the screen. That's how two fingers can press two buttons at the same time. Try it on a phone, or use the mouse:

Draw the buttons last, on top of everything else, and a bit darker while they're pressed. keys[button.code] tells you which ones are. Make them big: a button the size of a fingertip is still too small when you're in a hurry.

Build a virtual joystick

A button is either on or off. A virtual joystick also knows how far you push it, so the player can sneak or race. It appears where the thumb touches the screen. The farther the thumb moves from that spot, the faster the bee flies. At joystickRadius, 50 pixels away, the bee reaches full speed:

let joystick = {startX: 0, startY: 0, x: 0, y: 0};
const joystickRadius = 50;

canvas.addEventListener('pointerdown', function (event) {
    updatePointer(event);
    joystick.startX = pointer.x;
    joystick.startY = pointer.y;
    canvas.setPointerCapture(event.pointerId);
});

canvas.addEventListener('pointermove', function (event) {
    if (!pointer.isDown) {
        return;
    }
    updatePointer(event);

    // How far the thumb is from where it started
    let dx = pointer.x - joystick.startX;
    let dy = pointer.y - joystick.startY;
    let length = Math.sqrt(dx * dx + dy * dy);

    // Don't go past the edge of the joystick
    if (length > joystickRadius) {
        dx = dx / length * joystickRadius;
        dy = dy / length * joystickRadius;
    }

    // From -1 to 1 in both directions
    joystick.x = dx / joystickRadius;
    joystick.y = dy / joystickRadius;
});

canvas.addEventListener('pointerup', function () {
    joystick.x = 0;
    joystick.y = 0;
});

The listeners from the pointer events section still set pointer.isDown. Then update() moves the bee:

bee.x += joystick.x * speed * secondsPassed;
bee.y += joystick.y * speed * secondsPassed;

Don't normalize this one, because the length of the direction is the speed. Push the joystick halfway, and the bee flies at half speed. Press anywhere on the canvas and drag:

Live demo The code updates as you play around
joystick.x = 0;
joystick.y = 0;

The joystick is made of two circles: a big one with a radius of joystickRadius, centered where the thumb first touched the screen, and a small one that follows the thumb but never goes past the edge of the big one. Many mobile games keep the joystick on the left half of the screen and use the right half for buttons, like jump or shoot.

Combine all the controls in one place

You now have controls for the keyboard, the mouse, on-screen buttons and a joystick. So far, each one was a separate example with its own listeners. In a real game, you want several of them at once, without turning update() into a mess. So collect them in one function that returns the direction the player wants to go. update() doesn't care where the direction comes from:

function getDirection(obj) {
    let x = 0;
    let y = 0;

    // The arrow keys or WASD
    if (keys.ArrowLeft || keys.KeyA) {
        x -= 1;
    }
    if (keys.ArrowRight || keys.KeyD) {
        x += 1;
    }
    if (keys.ArrowUp || keys.KeyW) {
        y -= 1;
    }
    if (keys.ArrowDown || keys.KeyS) {
        y += 1;
    }

    // Hold the mouse or a finger down to fly toward it
    if (pointer.isDown) {
        x = pointer.x - obj.x;
        y = pointer.y - obj.y;

        // Stop when you get there
        if (x * x + y * y < 10 * 10) {
            x = 0;
            y = 0;
        }
    }

    // Give the direction a length of 1, so you fly
    // just as fast in every direction
    let length = Math.sqrt(x * x + y * y);
    if (length > 0) {
        x /= length;
        y /= length;
    }
    return {x: x, y: y};
}

getDirection() checks the keys first. If the mouse button or a finger is down, the direction to the pointer replaces the one from the keys. In that case, the direction points from obj, the bee you pass in, to the pointer. At the end, the direction gets a length of 1, so the bee flies at the same speed with every control. Only give the pointer one job, though. The same press triggers all three: following the pointer, the on-screen buttons and the joystick. So a press on a button would also send the bee toward that button. Want the joystick instead of following the pointer? Add the joystick's direction after normalizing, because how far you push the joystick sets the speed.

The same idea works for actions. The player could jump with the space bar, a click or a tap on a button. Let all of them set one value, like input.jump, and the rest of your game only has to read that.

Fly your own bee

Time to put it all together. At the end of the sprite animation tutorial, you had a swarm of bees bumping into each other. Now one of them is yours. The Player class extends Bee, so it flies, flaps its wings and bumps just like the others. The main difference is that its velocity comes from the controls:

class Player extends Bee {
    constructor(context, x, y) {
        super(context, x, y, 0, 0);

        // Heavier, so you push the other bees away
        this.mass = 3;

        // A bit bigger than the other bees, and so is the hitbox
        this.scale = 1.25;
        this.radius = 15 * this.scale;

        // Pixels per second
        this.speed = 200;

        // How far the dashes of the ring have moved
        this.dashOffset = 0;
    }

    update(secondsPassed) {
        // Move the dashes 20 pixels per second, so the ring turns
        this.dashOffset -= 20 * secondsPassed;

        // Fly in the direction of the controls
        let direction = getDirection(this);
        this.vx = direction.x * this.speed;
        this.vy = direction.y * this.speed;

        // Keep facing the same way when you stop
        let angle = this.angle;
        super.update(secondsPassed);
        if (direction.x == 0 && direction.y == 0) {
            this.angle = angle;
        }
    }

    draw() {
        // A dashed ring that turns,
        // so you can spot your own bee in the swarm
        this.context.strokeStyle = '#0099b0';
        this.context.lineWidth = 3;
        this.context.setLineDash([8, 7]);
        this.context.lineDashOffset = this.dashOffset;
        this.context.beginPath();
        this.context.arc(this.x, this.y, 34, 0, 2 * Math.PI);
        this.context.stroke();
        this.context.setLineDash([]);

        // Draw the bee bigger, scaled around its middle
        this.context.save();
        this.context.translate(this.x, this.y);
        this.context.scale(this.scale, this.scale);
        this.context.translate(-this.x, -this.y);
        super.draw();
        this.context.restore();
    }
}

Your bee is three times as heavy as the others, so you push them out of the way instead of the other way around. A collision does change its velocity, but on the next frame, the controls take over again. Each bee faces the way it flies, with an angle calculated from its velocity. When your bee stops, its velocity drops to zero. That gives an angle of 0, which would suddenly turn it to face right. That's why update() keeps the old angle. Two things make your bee easy to spot. First, it's bigger than the others. To draw it that way, use save(), translate(), scale() and restore(), the same trick you used to rotate the bees in the sprite animation tutorial. Second, it has a ring: a dashed line just like the ones in the draw on canvas tutorial. To make the ring turn, update() lowers dashOffset a little every frame, and draw() copies that value into lineDashOffset.

In createWorld(), the function that creates the swarm, add your bee in the middle of the canvas, at (375, 200). Add it to gameObjects after the other bees, so it's drawn on top of them:

player = new Player(context, 375, 200);
gameObjects.push(player);

And here it is. The demo below uses getDirection(), so you can fly with the arrow keys or WASD, or press and hold where you want to go, with your mouse or finger. Click the canvas and fly into the swarm:

Common questions about game controls

How do I add gamepad support?

Use the Gamepad API. A gamepad doesn't send an event when you push a stick or press a button, so you read it in your game loop. navigator.getGamepads() gives you the buttons and the axes of every connected gamepad. The axes go from -1 to 1, just like the virtual joystick, so they fit right into getDirection().

What's the difference between event.keyCode, event.key and event.code?

event.keyCode is a number, like 37 for the left arrow. It's deprecated, and the numbers can differ between browsers and keyboard layouts. event.key is what the key types or does, and event.code is the physical key on the keyboard, no matter what it types. Use event.code for controls where the key's position matters, like WASD. On a French keyboard, the keys in that spot type ZQSD, but event.code still gives 'KeyW', 'KeyA', 'KeyS' and 'KeyD'. Use event.key for text, like a name for the high score list, where you want the characters the player typed.

How do I let players change the controls?

Instead of hard-coding the keys, keep them in an object, like controls = {left: 'ArrowLeft', right: 'ArrowRight'}, and check keys[controls.left]. To change a key, wait for the next keydown and store its event.code. Save the choice in local storage, and it'll still be there next time.

How do I turn off the right-click menu on the canvas?

Listen for the contextmenu event on the canvas and call preventDefault(). Then you can use the right mouse button in your game. event.button tells you which button it was: 0 for the left, 1 for the middle and 2 for the right one.

How do I detect a long press or a double tap?

Store the time of the pointerdown, using event.timeStamp. If the pointer is still down half a second later, that's a long press. For a double tap, check if the previous tap was less than 300 milliseconds earlier and in about the same spot.

How do controls work in Phaser or PixiJS?

Phaser has its own input system for the keyboard, pointers and gamepads, built on the same browser events. PixiJS gives every object on the screen its own pointer events, so you don't have to check which object is under the pointer. For the keyboard, you write your own code in PixiJS, just like on this page.

The complete code

Here's the complete code for your game so far. It picks up where the previous tutorial left off, and it loads the sprite sheet from spicyyoghurt.com. The game is the only thing on its page, so init() gives the canvas focus right away with canvas.focus(), and the CSS turns off scrolling on the whole canvas with touch-action: none. Paste it into an HTML file and open it in your browser:

What's next?

Your bee now listens to the keyboard, the mouse and the touchscreen. You've learned how to keep track of which keys are pressed, make the bee fly at the same speed in every direction, drag objects, and build on-screen buttons and a virtual joystick. That's everything you need to control a game, on a computer and on a phone.

If you have any questions, feel free to ask them in the comment section. You can