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
- Coyote Time: Grants a short grace period (typically 50–150ms) allowing the player to jump after walking off an edge.
- Jump Buffering: Registers a jump input pressed shortly before landing (typically 50–150ms) and executes the jump automatically upon touching the ground.
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
- Walking off edges: When the player falls,
collisionEndcaptureslastGroundedTime. If the jump key is pressed withinCOYOTE_TIME, the condition passes and the jump executes. - Early jump input: When the player presses jump
while airborne,
lastJumpPressTimeis updated. If the player lands withinJUMP_BUFFER, the jump triggers instantly upon the first valid ground collision update. - Invalidation: Zeroing out both timestamps immediately after applying force prevents unintended chained jumps within the same grace periods.