0 Spicy Yoghurt | Last updated: 1 November 2026 | HTML5 game tutorial
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:
// 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:
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:
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:
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:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Game Controls Tutorial | Spicy Yoghurt</title>
<style>
#canvas {
width: 750px;
height: 400px;
border: 1px solid lightgray;
touch-action: none;
}
</style>
</head>
<body>
<canvas
id="canvas"
width="750"
height="400"
tabindex="0"
>
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
'use strict';
// Define the edges of the canvas
const canvasWidth = 750;
const canvasHeight = 400;
// The sprite sheet of the bee
const spriteUrl =
'https://spicyyoghurt.com/img/tutorials/' +
'develop_a_html5_game/bee-sprite-sheet.png';
let canvas;
let context;
let oldTimeStamp = 0;
let gameObjects = [];
let player;
// The game keys, by event.code
const gameKeys = [
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
'KeyW', 'KeyA', 'KeyS', 'KeyD'
];
// The keys that are down right now
let keys = {};
// The mouse or finger on the canvas
let pointer = {x: 0, y: 0, isDown: false};
class GameObject {
constructor(context, x, y, vx, vy, mass) {
this.context = context;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.mass = mass;
this.restitution = 0.9;
this.isColliding = false;
}
}
class Bee extends GameObject {
// One sprite sheet for all bees, with 4 columns and 2 rows
static sprite;
static numColumns = 4;
static numRows = 2;
static frameWidth = 128;
static frameHeight = 128;
// One animation for every row of the sprite sheet
static animations = {
fly: {row: 0, fps: 20, loop: true},
bump: {row: 1, fps: 10, loop: false}
};
constructor(context, x, y, vx, vy) {
super(context, x, y, vx, vy, 1);
// Keep flying at the same speed after a bump
this.restitution = 1;
// The hitbox is the round body of the bee
this.radius = 15;
// Face the direction of the velocity
this.angle = Math.atan2(vy, vx);
// Start at a random frame, so the bees don't flap in sync
this.animation = Bee.animations.fly;
this.currentFrame =
Math.floor(Math.random() * Bee.numColumns);
this.frameTimer = 0;
}
play(name) {
this.animation = Bee.animations[name];
this.currentFrame = 0;
this.frameTimer = 0;
}
bump() {
// Don't start over while the bee is still dizzy
if (this.animation != Bee.animations.bump) {
this.play('bump');
}
}
update(secondsPassed) {
// Move at the current velocity
this.x += this.vx * secondsPassed;
this.y += this.vy * secondsPassed;
// Calculate the angle, in radians
this.angle = Math.atan2(this.vy, this.vx);
// Pick the next frame when it's due
this.frameTimer += secondsPassed;
while (this.frameTimer >= 1 / this.animation.fps) {
this.frameTimer -= 1 / this.animation.fps;
this.currentFrame++;
if (this.currentFrame == Bee.numColumns) {
if (this.animation.loop) {
this.currentFrame = 0;
} else {
this.play('fly');
}
}
}
}
draw() {
// Save the context, move it to the bee and rotate it
this.context.save();
this.context.translate(this.x, this.y);
this.context.rotate(this.angle);
// Draw the image around (0, 0), the middle of the body
this.context.drawImage(
Bee.sprite,
this.currentFrame * Bee.frameWidth,
this.animation.row * Bee.frameHeight,
Bee.frameWidth, Bee.frameHeight,
-28, -32, 64, 64
);
// Put the context back the way it was
this.context.restore();
}
}
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();
}
}
window.onload = init;
function init() {
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// The keyboard, only when the canvas has focus
canvas.addEventListener('keydown', onKeyDown);
canvas.addEventListener('keyup', onKeyUp);
canvas.addEventListener('blur', releaseAllKeys);
canvas.focus();
// The mouse, a finger or a pen
canvas.addEventListener('pointerdown', onPointerDown);
canvas.addEventListener('pointermove', updatePointer);
canvas.addEventListener('pointerup', onPointerUp);
canvas.addEventListener('pointercancel', onPointerUp);
// Start the game when the sprite sheet has loaded
loadImage(spriteUrl).then(function (img) {
Bee.sprite = img;
createWorld();
window.requestAnimationFrame(gameLoop);
});
}
function loadImage(src) {
return new Promise(function (resolve, reject) {
let img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = reject;
img.src = src;
});
}
function resizeCanvas() {
// One canvas pixel for every pixel of the screen
let ratio = window.devicePixelRatio || 1;
canvas.width = Math.round(canvas.clientWidth * ratio);
canvas.height = Math.round(canvas.clientHeight * ratio);
// Keep drawing in game coordinates (750 x 400)
context.setTransform(
canvas.width / canvasWidth, 0,
0, canvas.height / canvasHeight,
0, 0
);
}
function createWorld() {
// A swarm of bees, flying in random directions
for (let i = 0; i < 12; i++) {
gameObjects.push(createBee(
50 + Math.random() * 650,
50 + Math.random() * 300
));
}
// Your own bee, in the middle of the canvas
player = new Player(context, 375, 200);
gameObjects.push(player);
}
function createBee(x, y) {
// Every bee flies at 100 pixels per second
let angle = Math.random() * 2 * Math.PI;
return new Bee(
context, x, y,
Math.cos(angle) * 100, Math.sin(angle) * 100
);
}
function onKeyDown(event) {
keys[event.code] = true;
// Don't let the game keys scroll the page
if (gameKeys.includes(event.code)) {
event.preventDefault();
}
}
function onKeyUp(event) {
keys[event.code] = false;
}
function releaseAllKeys() {
// The game can't see keys being released anymore
keys = {};
}
function onPointerDown(event) {
pointer.isDown = true;
updatePointer(event);
// Keep getting pointermove events, even outside the canvas
canvas.setPointerCapture(event.pointerId);
}
function onPointerUp() {
pointer.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;
}
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};
}
function gameLoop(timeStamp) {
// Calculate how much time has passed, capped at a maximum
let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
oldTimeStamp = timeStamp;
secondsPassed = Math.min(secondsPassed, 0.1);
// Loop over all game objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].update(secondsPassed);
}
detectCollisions();
detectEdgeCollisions();
// Clear the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Draw all game objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].draw();
}
window.requestAnimationFrame(gameLoop);
}
function detectCollisions() {
let obj1;
let obj2;
// Reset the collision state of all objects
for (let i = 0; i < gameObjects.length; i++) {
gameObjects[i].isColliding = false;
}
// Start checking for collisions
for (let i = 0; i < gameObjects.length; i++) {
obj1 = gameObjects[i];
for (let j = i + 1; j < gameObjects.length; j++) {
obj2 = gameObjects[j];
// Compare object1 with object2
if (!circleIntersect(
obj1.x, obj1.y, obj1.radius,
obj2.x, obj2.y, obj2.radius
)) {
continue;
}
obj1.isColliding = true;
obj2.isColliding = true;
// Find the direction of the collision
let vCollision = {
x: obj2.x - obj1.x,
y: obj2.y - obj1.y
};
let distance = Math.sqrt(
(obj2.x - obj1.x) * (obj2.x - obj1.x) +
(obj2.y - obj1.y) * (obj2.y - obj1.y)
);
// Two objects in the same spot have no direction
if (distance == 0) {
continue;
}
let vCollisionNorm = {
x: vCollision.x / distance,
y: vCollision.y / distance
};
// Push them apart, so they don't sink into each other
separate(obj1, obj2, vCollisionNorm, distance);
// Calculate the speed of the detected collision
let vRelativeVelocity = {
x: obj1.vx - obj2.vx,
y: obj1.vy - obj2.vy
};
let speed =
vRelativeVelocity.x * vCollisionNorm.x +
vRelativeVelocity.y * vCollisionNorm.y;
// The objects are already moving apart
if (speed < 0) {
continue;
}
// Use the lower restitution of the two objects
let restitution = Math.min(
obj1.restitution, obj2.restitution
);
// Change the velocity of both objects
let totalMass = obj1.mass + obj2.mass;
let impulse = (1 + restitution) * speed / totalMass;
obj1.vx -= (impulse * obj2.mass * vCollisionNorm.x);
obj1.vy -= (impulse * obj2.mass * vCollisionNorm.y);
obj2.vx += (impulse * obj1.mass * vCollisionNorm.x);
obj2.vy += (impulse * obj1.mass * vCollisionNorm.y);
// Both bees feel the bump
obj1.bump();
obj2.bump();
}
}
}
function circleIntersect(x1, y1, r1, x2, y2, r2) {
// Calculate the squared distance between the two centers
let squareDistance =
(x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
// When the distance is less than or equal to the sum
// of the two radii, the circles touch or overlap
return squareDistance <= ((r1 + r2) * (r1 + r2));
}
function separate(obj1, obj2, vCollisionNorm, distance) {
// How far the circles overlap
let overlap = obj1.radius + obj2.radius - distance;
// Move them apart along the collision normal
let totalMass = obj1.mass + obj2.mass;
obj1.x -= vCollisionNorm.x * overlap * obj2.mass / totalMass;
obj1.y -= vCollisionNorm.y * overlap * obj2.mass / totalMass;
obj2.x += vCollisionNorm.x * overlap * obj1.mass / totalMass;
obj2.y += vCollisionNorm.y * overlap * obj1.mass / totalMass;
}
function detectEdgeCollisions() {
let obj;
for (let i = 0; i < gameObjects.length; i++) {
obj = gameObjects[i];
// Check for left and right
if (obj.x < obj.radius) {
obj.vx = Math.abs(obj.vx) * obj.restitution;
obj.x = obj.radius;
obj.bump();
} else if (obj.x > canvasWidth - obj.radius) {
obj.vx = -Math.abs(obj.vx) * obj.restitution;
obj.x = canvasWidth - obj.radius;
obj.bump();
}
// Check for top and bottom
if (obj.y < obj.radius) {
obj.vy = Math.abs(obj.vy) * obj.restitution;
obj.y = obj.radius;
obj.bump();
} else if (obj.y > canvasHeight - obj.radius) {
obj.vy = -Math.abs(obj.vy) * obj.restitution;
obj.y = canvasHeight - obj.radius;
obj.bump();
}
}
}
</script>
</body>
</html>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 download the example code here.
Up next: Manage game states with a simple state machine
Your bee gets a real game to play, with a start screen, a score and a game over screen.
Comments
Thanks! We've received your comment. We check every comment by hand before it appears, so this can take a few days.

No comments yet. Be the first to leave one!