Lesson 13

Platformer feel: the invisible forgiveness that makes jumps fair.

A platformer can have correct physics and still feel bad. Good feel comes from acceleration, friction, coyote time, jump buffering and collision rules that match what the player thinks happened.

IntermediatePlatformersPhysics Feel35-55 minutes

1. Start with acceleration, not instant speed

Instant horizontal speed makes a character feel like a cursor. Acceleration and friction make movement readable. The player presses left or right, velocity moves toward a maximum, and when the input stops, friction slows the character down. This gives jumps a small arc and makes landing adjustments possible.

const MOVE_SPEED = 4.2;
const ACCEL = 0.45;
const FRICTION = 0.82;

function updateHorizontal(player, keys) {
    if (keys.left || keys.right) {
        const dir = keys.left ? -1 : 1;
        player.vx += dir * ACCEL;
        player.vx = Math.max(-MOVE_SPEED, Math.min(MOVE_SPEED, player.vx));
        player.facingRight = dir > 0;
    } else {
        player.vx *= FRICTION;
        if (Math.abs(player.vx) < 0.08) player.vx = 0;
    }

    player.x += player.vx;
}

Source pattern: Supagames level 24 platformers use velocity, friction, coyote windows, double jumps and wall interactions rather than teleport-like movement.

Small numbers matter. If acceleration is too high, movement feels twitchy. If friction is too low, landing feels slippery. Tune while playing, not by staring at constants.

2. Coyote time fixes edge frustration

Coyote time means the player can still jump for a few frames after leaving a platform. It is named after cartoon logic: the character hangs in the air just long enough for the player's intent to count. Without it, players press jump near an edge and the game says no, even though visually it felt like they were still grounded.

const COYOTE_FRAMES = 6;
const JUMP_VELOCITY = -10.5;

function updateJumpTimers(player) {
    if (player.onGround) {
        player.coyoteTimer = COYOTE_FRAMES;
    } else if (player.coyoteTimer > 0) {
        player.coyoteTimer--;
    }
}

function tryJump(player, input) {
    if (!input.jumpPressed) return false;
    if (player.onGround || player.coyoteTimer > 0) {
        player.vy = JUMP_VELOCITY;
        player.onGround = false;
        player.coyoteTimer = 0;
        return true;
    }
    return false;
}

Six frames at 60 FPS is only a tenth of a second, but it changes the feel dramatically. It does not make the game easy; it makes the game honor intent.

3. Jump buffering fixes early presses

The opposite problem happens when the player presses jump just before landing. A strict game ignores the input because the character is still airborne. Jump buffering stores that input for a few frames and consumes it when the character lands.

const JUMP_BUFFER_FRAMES = 7;

function updateJumpBuffer(input) {
    if (input.jumpPressed) {
        input.jumpBuffer = JUMP_BUFFER_FRAMES;
    } else if (input.jumpBuffer > 0) {
        input.jumpBuffer--;
    }
}

function consumeBufferedJump(player, input) {
    if (input.jumpBuffer > 0 && (player.onGround || player.coyoteTimer > 0)) {
        player.vy = JUMP_VELOCITY;
        player.onGround = false;
        player.coyoteTimer = 0;
        input.jumpBuffer = 0;
    }
}

Coyote time forgives slightly late jumps. Jump buffering forgives slightly early jumps. Together they make platformers feel fair without changing level design.

4. Double jump and wall jump need clear state

Additional moves are fun only when the rules are obvious. Track how many air jumps remain and reset them on landing. If wall jumping exists, decide whether it resets jumps, consumes a jump or has its own cooldown. Hidden exceptions create bugs and player confusion.

function tryAirMove(player, input) {
    if (!input.jumpPressed) return;

    if (player.onWall && !player.onGround) {
        player.vy = JUMP_VELOCITY * 0.9;
        player.vx = -player.wallSide * 6;
        player.airJumpsUsed = 0;
        return;
    }

    if (!player.onGround && player.airJumpsUsed < player.maxAirJumps) {
        player.vy = -9.2;
        player.airJumpsUsed++;
        player.spinTimer = 18;
    }
}

Visual feedback matters here. A double jump should have a particle burst or animation so the player understands that a limited resource was spent. A wall slide should slow the fall enough to be noticed.

5. Resolve platform collisions from the previous position

Most "I fell through the platform" bugs come from collision checks that only look at the final position. For platform landings, compare the player's previous bottom with the platform top. If the previous bottom was above the platform and the new bottom crossed it while falling, snap the player to the top.

function resolvePlatforms(player, platforms) {
    player.onGround = false;

    for (const p of platforms) {
        const overlapsX = player.x < p.x + p.w && player.x + player.w > p.x;
        if (!overlapsX) continue;

        const wasAbove = player.prevY + player.h <= p.y + 8;
        const crossedTop = player.y + player.h >= p.y;

        if (player.vy >= 0 && wasAbove && crossedTop) {
            player.y = p.y - player.h;
            player.vy = 0;
            player.onGround = true;
            player.airJumpsUsed = 0;
        }
    }
}

This is also where moving platforms, bouncy blocks and crumbling platforms can hook in. Keep the basic landing rule clear first, then add special behavior.

6. Camera feel is part of movement

If the player moves perfectly but the camera jitters, the game still feels broken. Smooth the camera toward a target and clamp it to world bounds. In vertical platformers, bias the target slightly above the player so jumps reveal where the player is going.

function updateCamera(camera, player, canvas, world) {
    const targetX = player.x - canvas.width / 2;
    const targetY = player.y - canvas.height / 2 - 80;

    camera.x += (targetX - camera.x) * 0.14;
    camera.y += (targetY - camera.y) * 0.11;
    camera.x = Math.max(0, Math.min(world.w - canvas.width, camera.x));
    camera.y = Math.max(0, Math.min(world.h - canvas.height, camera.y));
}

When testing, deliberately jump at platform edges, hold jump too early, tap jump too late and land while changing direction. Those moments reveal whether the game is fair. Good platformer feel is a collection of tiny kindnesses that the player may never name, but will absolutely feel.

Previous: Dialogue and quests. Next: Racing physics and handling.