Implement Coyote Time and Jump Buffers in Matter.js

This guide explains how to implement responsive platformer jump mechanics—specifically coyote time and jump buffering—using Matter.js collision events and timestamps. By tracking the exact time a player body leaves a surface and the time a jump input is triggered, you can eliminate unresponsive controls and missed inputs, creating a fluid and forgiving jumping mechanic.


Understanding the Mechanics

Using performance.now() alongside Matter.js collision hooks provides an accurate, frame-rate independent way to calculate these intervals.


1. State Configuration

Define the player tracking variables and tolerance thresholds:

const JUMP_FORCE = -0.05; // Matter.js upward force
const COYOTE_TIME = 100;  // Milliseconds
const JUMP_BUFFER = 100;  // Milliseconds

let lastGroundedTime = 0;
let lastJumpPressTime = 0;
let isGrounded = false;

2. Tracking Ground Contact with Collision Events

To avoid false positives from wall collisions, designate a specific ground sensor on your player or verify collision normal vectors using Matter.js collision events.

Use collisionStart, collisionActive, and collisionEnd to update ground state and timestamps:

const { Events } = Matter;

// Detect landing or continued ground contact
Events.on(engine, 'collisionActive', (event) => {
    event.pairs.forEach((pair) => {
        if (isPlayerGroundCollision(pair, playerBody)) {
            isGrounded = true;
            lastGroundedTime = performance.now();
        }
    });
});

// Detect when leaving the ground
Events.on(engine, 'collisionEnd', (event) => {
    event.pairs.forEach((pair) => {
        if (isPlayerGroundCollision(pair, playerBody)) {
            isGrounded = false;
            // Record departure timestamp for coyote time
            lastGroundedTime = performance.now();
        }
    });
});

// Helper function to validate ground contact via normal or body label
function isPlayerGroundCollision(pair, player) {
    const { bodyA, bodyB, collision } = pair;
    
    if (bodyA === player || bodyB === player) {
        // Ensure collision normal points upward toward the player
        const normal = collision.normal;
        const playerIsA = bodyA === player;
        const yNormal = playerIsA ? -normal.y : normal.y;
        
        return yNormal < -0.5; // Surface angle qualifies as walkable ground
    }
    return false;
}

3. Buffering the Jump Input

Instead of triggering the jump physics immediately on keypress, log the timestamp of the event:

window.addEventListener('keydown', (e) => {
    if (e.code === 'Space' || e.code === 'ArrowUp') {
        lastJumpPressTime = performance.now();
    }
});

4. Executing the Jump in the Update Loop

In your main simulation loop (or inside Matter.Events.on(engine, 'beforeUpdate', ...)), evaluate whether both the coyote time and jump buffer conditions are met:

Events.on(engine, 'beforeUpdate', () => {
    const currentTime = performance.now();

    const canCoyoteJump = (currentTime - lastGroundedTime) <= COYOTE_TIME;
    const hasBufferedJump = (currentTime - lastJumpPressTime) <= JUMP_BUFFER;

    if (canCoyoteJump && hasBufferedJump) {
        executeJump(playerBody);
        
        // Invalidate timestamps to prevent double execution
        lastJumpPressTime = 0;
        lastGroundedTime = 0;
        isGrounded = false;
    }
});

function executeJump(body) {
    // Reset vertical velocity to ensure consistent jump height
    Matter.Body.setVelocity(body, { x: body.velocity.x, y: 0 });
    
    // Apply upward impulse
    Matter.Body.applyForce(body, body.position, { x: 0, y: JUMP_FORCE });
}

Summary of Execution Flow

  1. Walking off edges: When the player falls, collisionEnd captures lastGroundedTime. If the jump key is pressed within COYOTE_TIME, the condition passes and the jump executes.
  2. Early jump input: When the player presses jump while airborne, lastJumpPressTime is updated. If the player lands within JUMP_BUFFER, the jump triggers instantly upon the first valid ground collision update.
  3. Invalidation: Zeroing out both timestamps immediately after applying force prevents unintended chained jumps within the same grace periods.