How to Implement Variable Jump Height in Matter.js

This guide explains how to implement variable height jumping in Matter.js, allowing a character to jump higher the longer the player holds the jump button. By tracking user input and dynamically modifying body velocity or applying upward force over time, you can achieve responsive, platformer-style jumping physics instead of a fixed, rigid-body arc.

The Jump Truncation Approach

The most reliable and responsive way to handle variable jumping in 2D physics engines is the velocity cutoff method. When the jump key is pressed, the character immediately launches upward with maximum jump velocity. If the player releases the button before reaching the peak of the jump, the upward velocity is truncated (scaled down), causing the character to fall sooner.

Core Implementation Logic

To implement this mechanic, you need:

  1. Ground Detection: Ensure the player only initiates a jump when standing on a surface.
  2. Initial Impulse: Set the full upward velocity immediately upon key press.
  3. Release Truncation: Cut the vertical velocity if the key is released while the body is still moving upward.

Code Example

Below is a minimal JavaScript implementation using Matter.js:

const { Engine, Render, Runner, Bodies, Composite, Body, Events } = Matter;

// 1. Setup Engine and World
const engine = Engine.create();
const world = engine.world;

const player = Bodies.rectangle(400, 500, 40, 60, {
  inertia: Infinity, // Prevents player from rotating
  friction: 0.05
});

const ground = Bodies.rectangle(400, 580, 800, 40, { isStatic: true });
Composite.add(world, [player, ground]);

// 2. Jump Configuration Variables
const MAX_JUMP_VELOCITY = -12; // Initial upward velocity
const CUTOFF_FACTOR = 0.4;     // Multiplier applied when button is released early
let isGrounded = false;
let isJumping = false;

// 3. Ground Collision Detection
Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    if (pair.bodyA === player || pair.bodyB === player) {
      // Basic check: Ensure collision is below the player
      isGrounded = true;
      isJumping = false;
    }
  });
});

Events.on(engine, 'collisionEnd', (event) => {
  event.pairs.forEach((pair) => {
    if (pair.bodyA === player || pair.bodyB === player) {
      isGrounded = false;
    }
  });
});

// 4. Input Handling
window.addEventListener('keydown', (event) => {
  if (event.code === 'Space' && isGrounded) {
    // Apply full jump strength immediately
    Body.setVelocity(player, {
      x: player.velocity.x,
      y: MAX_JUMP_VELOCITY
    });
    isJumping = true;
    isGrounded = false;
  }
});

window.addEventListener('keyup', (event) => {
  if (event.code === 'Space' && isJumping) {
    // If released while moving upwards, dampen the vertical velocity
    if (player.velocity.y < 0) {
      Body.setVelocity(player, {
        x: player.velocity.x,
        y: player.velocity.y * CUTOFF_FACTOR
      });
    }
    isJumping = false;
  }
});

Alternative: Continuous Force Application

Another approach is to apply a smaller force continuously across engine update ticks while the key is held:

  1. On keydown, set an initial upward velocity and start a frame counter.
  2. Inside the beforeUpdate event, check if the jump key is still held and if the counter has not exceeded the maximum hold duration (e.g., 15 frames).
  3. Call Body.applyForce() upward on each valid frame.
  4. On keyup or when the frame limit is reached, stop applying force.

The velocity cutoff method is generally preferred over continuous force because it offers immediate responsiveness upon pressing the button while maintaining consistent physics behavior.