How to Build a Foosball Table in Matter.js

This guide explains how to model and implement a functional foosball table in Matter.js, complete with sliding and rotating player rods. You will learn how to initialize the physics environment, construct the playfield and goal boundaries, model multi-player rods using compound bodies or constraints, and map user controls to enable both lateral sliding and kicking rotation.

1. Setting Up the Matter.js Environment

Begin by setting up the core Matter.js modules: Engine, Render, Runner, Bodies, Composite, Body, and Constraint.

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

const engine = Engine.create();
engine.gravity.y = 0; // Top-down view requires zero gravity

const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 800,
    height: 500,
    wireframes: false
  }
});

Render.run(render);
Runner.run(Runner.create(), engine);

2. Creating the Table Boundaries and Ball

The table consists of outer walls with openings at both ends for goals. The ball is a high-restitution circle placed in the center.

const wallOptions = { isStatic: true, restitution: 0.8 };
const tableWidth = 800;
const tableHeight = 500;
const goalSize = 120;
const wallThickness = 20;

const topWall = Bodies.rectangle(tableWidth / 2, wallThickness / 2, tableWidth, wallThickness, wallOptions);
const bottomWall = Bodies.rectangle(tableWidth / 2, tableHeight - wallThickness / 2, tableWidth, wallThickness, wallOptions);

// Left goal posts
const leftTop = Bodies.rectangle(wallThickness / 2, (tableHeight - goalSize) / 4, wallThickness, (tableHeight - goalSize) / 2, wallOptions);
const leftBottom = Bodies.rectangle(wallThickness / 2, tableHeight - (tableHeight - goalSize) / 4, wallThickness, (tableHeight - goalSize) / 2, wallOptions);

// Right goal posts
const rightTop = Bodies.rectangle(tableWidth - wallThickness / 2, (tableHeight - goalSize) / 4, wallThickness, (tableHeight - goalSize) / 2, wallOptions);
const rightBottom = Bodies.rectangle(tableWidth - wallThickness / 2, tableHeight - (tableHeight - goalSize) / 4, wallThickness, (tableHeight - goalSize) / 2, wallOptions);

// Ball
const ball = Bodies.circle(tableWidth / 2, tableHeight / 2, 12, {
  restitution: 0.9,
  frictionAir: 0.01,
  density: 0.05
});

Composite.add(engine.world, [topWall, bottomWall, leftTop, leftBottom, rightTop, rightBottom, ball]);

3. Constructing Sliding and Rotating Rods

In a 2D top-down view, foosball rods run vertically across the pitch. They require two degrees of freedom:

  1. Linear translation (sliding): Moving up and down along the Y-axis.
  2. Rotation (striking): Rotating forward or backward to simulate kicking the ball.

The most robust way to build a rod in Matter.js is to create a compound body composed of the rod shaft and its attached player figures.

function createRod(x, playerOffsets, minLimitY, maxLimitY) {
  const rodWidth = 8;
  const rodHeight = 400;
  
  // Invisible or visual rod bar
  const rodBar = Bodies.rectangle(x, tableHeight / 2, rodWidth, rodHeight, {
    isSensor: true,
    render: { fillStyle: '#888888' }
  });

  // Create player parts relative to rod center
  const playerParts = playerOffsets.map(offsetY => {
    return Bodies.rectangle(x, tableHeight / 2 + offsetY, 20, 30, {
      chamfer: { radius: 4 },
      render: { fillStyle: '#e74c3c' }
    });
  });

  // Combine into a single compound body
  const rodCompound = Body.create({
    parts: [rodBar, ...playerParts],
    frictionAir: 0.05,
    mass: 10
  });

  // Store metadata for sliding constraints
  rodCompound.customLimits = {
    minY: minLimitY,
    maxY: maxLimitY,
    defaultX: x
  };

  Composite.add(engine.world, rodCompound);
  return rodCompound;
}

// Example: 3-player attacking rod
const attackRod = createRod(300, [-100, 0, 100], 180, 320);

4. Handling Movement and Controls

To enforce the mechanical constraints of real rods without relying solely on complex joint physics, lock the horizontal position while allowing controlled sliding along the Y-axis and controlled kicking rotation.

const keys = {};
window.addEventListener('keydown', (e) => { keys[e.key] = true; });
window.addEventListener('keyup', (e) => { keys[e.key] = false; });

Matter.Events.on(engine, 'beforeUpdate', () => {
  const moveSpeed = 4;
  const kickTorque = 0.15;

  // Linear Sliding (Up / Down)
  let targetVelocityY = 0;
  if (keys['ArrowUp'] || keys['w']) targetVelocityY = -moveSpeed;
  if (keys['ArrowDown'] || keys['s']) targetVelocityY = moveSpeed;

  Body.setVelocity(attackRod, { x: 0, y: targetVelocityY });

  // Keep rod pinned to its designated X track and clamped within vertical limits
  const currentY = attackRod.position.y;
  const clampedY = Math.max(attackRod.customLimits.minY, Math.min(attackRod.customLimits.maxY, currentY));
  
  Body.setPosition(attackRod, {
    x: attackRod.customLimits.defaultX,
    y: clampedY
  });

  // Rod Rotation (Kicking motion)
  if (keys[' ']) {
    // Swing forward when space is pressed
    Body.applyAngularImpulse(attackRod, kickTorque);
  } else {
    // Return rod to neutral vertical position using spring-back damping
    const restAngle = 0;
    const angleDifference = restAngle - attackRod.angle;
    Body.setAngularVelocity(attackRod, attackRod.angularVelocity * 0.8 + angleDifference * 0.05);
  }
});

5. Adding Multiple Rods and Collision Tuning

Repeat the rod creation across standard positions:

To avoid erratic physics when players strike the ball at high speeds, adjust the solver parameters on the engine:

engine.positionIterations = 10;
engine.velocityIterations = 10;

This ensures accurate collision detection between the rotating rectangular player bodies and the high-velocity ball.