0 Spicy Yoghurt | Last updated: 25 October 2026 | HTML5 game tutorial
Add keyboard, mouse and touch controls
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 the swarm.
Part of a series - In this series, you make an HTML5 game step by step. Here, you take control of one of the bees from the sprite animation part.
Here's what you'll build. Click the canvas and fly the bee with the arrow keys, or press and hold with your mouse or finger where you want it to go:
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);
});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, and the player can click it or tab to it, just like a button or a text field:
<canvas id="canvas" width="750" height="400" tabindex="0"></canvas>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' 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. With event.key, a French player would have to reach for keys all over the keyboard. 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. The obvious next step is to move the bee every time an arrow key goes down:
canvas.addEventListener('keydown', function (event) {
if (event.code == 'ArrowRight') {
bee.x += 10;
}
if (event.code == 'ArrowLeft') {
bee.x -= 10;
}
});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 from the first demo: the bee only moves when a keydown event comes in. So the speed of your game depends on the player's keyboard settings, 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. Every frame, the game loop checks which keys are down and moves the bee. 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, using the time 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. First it reads the input, and then it updates, 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. That's 283 pixels per second along the diagonal, 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, and then give that direction a length of 1. 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;Diagonally, x and y both become 0.71 instead of 1, and 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 say Click to play when they don't have it. 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 event gives you clientX and clientY, the position on the screen. Your game needs the position in its own coordinates, because the canvas is scaled 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:
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) {
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 check for 10 pixels, it would overshoot the pointer every frame and jitter back and forth.
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;
}Loop over the bees and check each one. Go through the list backward, because the bee that's drawn last is on top:
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;
}
}
});The hitbox of a bee 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
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);
}
});
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: the distance of each move, divided by the time since the move before it. When you let go, give the bee that speed, 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 does it another way. It calls preventDefault() on the touchstart event, but only when your finger lands on a bee. 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() 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. 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, up to a maximum:
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 two circles: a big one where the thumb went down, and a small one that follows the thumb, but never 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. 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};
}The keys come first. If the mouse or a finger is down, it takes over. At the end, the direction gets a length of 1, so the bee flies at the same speed with every control. Want the joystick in there too? Add it after the normalization, because its length is 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 only 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;
}
update(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 circle, so you can spot your own bee in the swarm
this.context.fillStyle = 'rgba(0, 153, 176, 0.3)';
this.context.beginPath();
this.context.arc(this.x, this.y, 30, 0, 2 * Math.PI);
this.context.fill();
// 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. Stopping would turn the bee to face right, because a velocity of zero gives an angle of 0. That's why update() keeps the old angle. Drawing the bee bigger takes save(), translate(), scale() and restore(), just like rotating an image.
In createWorld(), add your bee to the game objects, after the swarm:
player = new Player(context, 375, 200);
gameObjects.push(player);And here it is. 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 the character the key types, and event.code is the key's position on the keyboard. For controls, use event.code.
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. 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;
}
update(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 circle, so you can spot your own bee in the swarm
this.context.fillStyle = 'rgba(0, 153, 176, 0.3)';
this.context.beginPath();
this.context.arc(this.x, this.y, 30, 0, 2 * Math.PI);
this.context.fill();
// 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.
You've reached the final tutorial in this series. At least for now, that is. We're working hard to create the next one, so check back every now and then!
If you'd like to support Spicy Yoghurt, please share our articles or follow us on social media.
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!