Build Spring-Loaded Pinball Bumpers in Matter.js

This guide explains how to construct high-energy, spring-loaded pinball bumpers in Matter.js that launch balls outward upon contact. While standard physics materials rely on restitution (bounciness) capped at energy conservation limits, simulating an active mechanical bumper requires intercepting collision events and calculating an outward radial impulse. By combining static circular bodies, vector math, and instantaneous velocity overrides, you can achieve responsive and punchy pinball bumper mechanics.

1. Creating the Bumper Body

A pinball bumper is typically fixed in place while interacting dynamically with the ball. Define the bumper as a static circle and attach a custom label so it can be easily identified inside collision event listeners.

const { Bodies, World } = Matter;

const bumper = Bodies.circle(400, 300, 40, {
  isStatic: true,
  label: 'bumper',
  render: {
    fillStyle: '#e74c3c'
  }
});

World.add(engine.world, bumper);

2. Detecting the Collision

Matter.js provides collision hooks through its Events module. Listen for the collisionStart event to detect the exact frame the ball contacts the bumper.

Matter.Events.on(engine, 'collisionStart', (event) => {
  const pairs = event.pairs;

  for (let i = 0; i < pairs.length; i++) {
    const { bodyA, bodyB } = pairs[i];

    if (bodyA.label === 'bumper' && bodyB.label === 'ball') {
      triggerBumperBoost(bodyA, bodyB);
    } else if (bodyB.label === 'bumper' && bodyA.label === 'ball') {
      triggerBumperBoost(bodyB, bodyA);
    }
  }
});

3. Calculating the Radial Vector

To throw the ball directly away from the bumper regardless of its entry angle, compute a directional unit vector extending from the bumper’s center toward the ball’s center.

function triggerBumperBoost(bumperBody, ballBody) {
  const boostMagnitude = 20; // Adjust for desired launch power

  // Calculate direction vector from bumper to ball
  const deltaX = ballBody.position.x - bumperBody.position.x;
  const deltaY = ballBody.position.y - bumperBody.position.y;
  
  // Calculate distance
  const distance = Math.hypot(deltaX, deltaY) || 1;

  // Normalize the direction vector
  const normalX = deltaX / distance;
  const normalY = deltaY / distance;

  // Set the instantaneous outward velocity
  Matter.Body.setVelocity(ballBody, {
    x: normalX * boostMagnitude,
    y: normalY * boostMagnitude
  });
}

Using Matter.Body.setVelocity completely overrides incoming momentum, guaranteeing consistent and arcade-accurate outward ejection. If you prefer to combine incoming momentum with the boost instead of overriding it, replace setVelocity with Matter.Body.applyForce:

Matter.Body.applyForce(ballBody, ballBody.position, {
  x: normalX * forceMagnitude,
  y: normalY * forceMagnitude
});

4. Simulating Spring Compression Visually

Because true mechanical springs can cause tunneling or instability when colliding at high speeds, handling the outward boost programmatically while animating the visual spring recoil provides the best stability. You can scale down the bumper momentarily when struck and interpolate it back to its resting state:

function triggerBumperBoost(bumperBody, ballBody) {
  // Apply outward velocity
  const boostMagnitude = 22;
  const deltaX = ballBody.position.x - bumperBody.position.x;
  const deltaY = ballBody.position.y - bumperBody.position.y;
  const distance = Math.hypot(deltaX, deltaY) || 1;

  Matter.Body.setVelocity(ballBody, {
    x: (deltaX / distance) * boostMagnitude,
    y: (deltaY / distance) * boostMagnitude
  });

  // Visual spring compression effect
  Matter.Body.scale(bumperBody, 0.9, 0.9);
  setTimeout(() => {
    Matter.Body.scale(bumperBody, 1 / 0.9, 1 / 0.9);
  }, 60);
}

This decoupled approach ensures reliable physics calculations while maintaining the satisfying visual kick of a mechanical pinball bumper.