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:
- Linear translation (sliding): Moving up and down along the Y-axis.
- 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:
- Goalkeeper / Defense Rods: Fewer players, wider sliding limits.
- Midfield Rods: Four to five players, restricted vertical travel.
- Attack Rods: Two to three players, balanced travel.
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.