Jumping
This tutorial switches from top-down to side-scrolling view. The hero moves left and right with arrow keys and jumps with spacebar. The jump mechanic is built on two properties: an initial upward velocity and a per-frame gravity that pulls the hero back down.
Jump Physics: Making It Feel Right
In this coordinate system, moving up means decreasing Y. Pressing spacebar sets velocityY to a negative value like -15 pixels per frame. Each frame after that, gravity increments velocityY back toward positive (downward):
// Each frame, gravity makes you fall faster
player.velocityY += gravity;
player.y += player.velocityY;
Watch this in action: Starting jump speed -11, gravity 0.8:
- Frame 1: velocity = -11, move up 11 pixels
- Frame 2: velocity = -10.2, move up 10.2 pixels
- Frame 3: velocity = -9.4, move up 9.4 pixels
- …eventually velocity = 0 (peak of jump)
- Then velocity becomes positive and you fall!
Gravity values control fall feel: 0.3 is floaty, 1.2 is snappy. Different characters can have separate gravity values.
Collision Response
When the player contacts a surface:
- Hit ceiling while jumping up? Set
velocityY = 0and start falling - Land on ground while falling? Set
velocityY = 0and allow jumping again - Walk off a platform? Start falling immediately!
Keep velocities smaller than the tile size. If velocityY exceeds TILE_SIZE in a single frame, the player can skip past a one-tile-thick floor without triggering the collision check.
Horizontal and vertical velocities are independent — left/right movement is unaffected by jumping or falling.
Player Properties
The player object needs a few extra properties to support jumping:
const player = {
x: 100,
y: 200,
width: 12,
height: 12,
velocityX: 0,
velocityY: 0,
speed: 2, // Left/right movement speed
jumpPower: -11, // Initial upward velocity (negative = up)
gravity: 0.8, // Added to velocityY each frame
onGround: false // Prevents jumping while airborne
};
Spawn Position
Place the hero on top of a floor tile rather than in open space. Start with onGround: true and velocityY: 0 so the hero doesn’t fall through the floor on the first frame:
function placeHeroOnGround(tileX, tileY) {
player.x = tileX * TILE_SIZE;
player.y = (tileY + 1) * TILE_SIZE - player.height;
player.onGround = true;
player.velocityY = 0;
}
Input Handling
Track which keys are held with a keys object updated by event listeners:
const keys = {};
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
// Prevents spacebar from scrolling the page
if (['Space', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
if (['Space', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.code)) {
e.preventDefault();
}
});
function handleInput() {
player.velocityX = 0;
if (keys['ArrowLeft']) player.velocityX = -player.speed;
if (keys['ArrowRight']) player.velocityX = player.speed;
if (keys['Space'] && player.onGround) {
player.velocityY = player.jumpPower;
player.onGround = false;
}
}
Checking player.onGround before applying jumpPower prevents the player from jumping while airborne. The e.preventDefault() calls prevent the browser from scrolling the page when spacebar or arrow keys are pressed — without them the page scrolls instead of the hero jumping.
Physics Update
Each frame, apply gravity and move the player by their current velocity:
function updatePhysics() {
player.velocityY += player.gravity;
// Cap fall speed — if velocityY exceeds TILE_SIZE the player
// can skip past a one-tile floor in a single frame
const maxFallSpeed = TILE_SIZE * 0.8;
if (player.velocityY > maxFallSpeed) player.velocityY = maxFallSpeed;
player.x += player.velocityX;
player.y += player.velocityY;
player.x = Math.max(0, Math.min(player.x, 300 - player.width));
}
Separating input, physics, and collision into distinct functions makes each piece straightforward to modify independently:
function gameLoop() {
handleInput();
updatePhysics();
checkCollisions();
}
Collision Detection
The isSolid helper checks a single pixel coordinate against the map array:
function isSolid(x, y) {
const col = Math.floor(x / TILE_SIZE);
const row = Math.floor(y / TILE_SIZE);
if (row < 0 || row >= map.length || col < 0 || col >= map[0].length) return true;
return map[row][col] === 1;
}
Collision runs separately for vertical and horizontal movement. Checking two points (left foot and right foot, or left and right of the head) catches contacts that a single midpoint check would miss:
function checkCollisions() {
// Falling: will the player's bottom overlap a tile?
if (player.velocityY > 0) {
const newBottom = player.y + player.height + player.velocityY;
if (isSolid(player.x + 2, newBottom) || isSolid(player.x + player.width - 2, newBottom)) {
player.y = Math.floor(newBottom / TILE_SIZE) * TILE_SIZE - player.height;
player.velocityY = 0;
player.onGround = true;
} else {
player.y += player.velocityY;
}
} else if (player.velocityY < 0) {
// Rising: will the player's top overlap a tile?
const newTop = player.y + player.velocityY;
if (isSolid(player.x + 2, newTop) || isSolid(player.x + player.width - 2, newTop)) {
player.y = Math.ceil(newTop / TILE_SIZE) * TILE_SIZE;
player.velocityY = 0;
} else {
player.y += player.velocityY;
}
}
}
Edge Detection
When the player walks off a platform’s edge, onGround needs to become false so gravity resumes. Check whether there is still solid ground beneath the player’s feet each frame:
if (player.onGround) {
if (!isSolid(player.x + 2, player.y + player.height + 1) &&
!isSolid(player.x + player.width - 2, player.y + player.height + 1)) {
player.onGround = false;
}
}
The +1 offset tests one pixel below the feet — if neither foot is over a solid tile, gravity takes over next frame.
Next: Clouds