How to Build a Pachinko Machine in Matter.js

This guide explains how to construct a functional Pachinko machine with hundreds of deflection pins using the Matter.js 2D physics engine. You will learn how to initialize the physics environment, generate an aligned grid of static pins using nested loops, configure realistic bouncing properties, build catch basins at the bottom, and optimize performance so the simulation runs at a stable 60 frames per second.

1. Initialize the Matter.js Environment

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

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

const engine = Engine.create();
const world = engine.world;

const width = 600;
const height = 800;

const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: width,
    height: height,
    wireframes: false,
    background: '#111'
  }
});

Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

2. Build the Outer Frame and Scoring Bins

Construct the outer boundaries to keep the balls inside the play area. Add vertical dividers at the bottom of the canvas to act as collection slots.

const wallOptions = { isStatic: true, render: { fillStyle: '#333' } };

// Outer walls
const leftWall = Bodies.rectangle(10, height / 2, 20, height, wallOptions);
const rightWall = Bodies.rectangle(width - 10, height / 2, 20, height, wallOptions);
const ground = Bodies.rectangle(width / 2, height - 10, width, 20, wallOptions);

Composite.add(world, [leftWall, rightWall, ground]);

// Bottom catchers / bins
const binCount = 7;
const binHeight = 100;
const binSpacing = width / binCount;

for (let i = 1; i < binCount; i++) {
  const x = i * binSpacing;
  const divider = Bodies.rectangle(x, height - binHeight / 2, 8, binHeight, wallOptions);
  Composite.add(world, divider);
}

3. Generate Hundreds of Deflection Pins

To achieve the classic Pachinko look, arrange pins in a staggered triangular or hexagonal grid. Use nested loops to generate rows and columns, offsetting every alternating row.

Setting isStatic: true ensures the pins do not move when struck by the balls.

const pinRadius = 4;
const rows = 14;
const cols = 15;
const startY = 120;
const rowSpacing = 35;
const colSpacing = 36;
const pins = [];

for (let row = 0; row < rows; row++) {
  const isStaggered = row % 2 !== 0;
  const xOffset = isStaggered ? colSpacing / 2 : 0;
  const currentCols = isStaggered ? cols - 1 : cols;
  const startX = (width - (currentCols - 1) * colSpacing) / 2;

  for (let col = 0; col < currentCols; col++) {
    const x = startX + col * colSpacing;
    const y = startY + row * rowSpacing;

    const pin = Bodies.circle(x, y, pinRadius, {
      isStatic: true,
      restitution: 0.6, // High bounciness
      friction: 0.05,
      render: { fillStyle: '#e0af00' }
    });

    pins.push(pin);
  }
}

// Add all pins in a single batch to maximize performance
Composite.add(world, pins);

4. Drop the Pachinko Balls

Create a function to spawn dynamic balls near the top of the board. Assigning a high restitution (elasticity) creates erratic, entertaining deflections as balls hit the pins.

function dropBall() {
  const jitter = (Math.random() - 0.5) * 60;
  const ball = Bodies.circle(width / 2 + jitter, 40, 8, {
    restitution: 0.7,
    friction: 0.01,
    density: 0.04,
    render: { fillStyle: '#ff3b30' }
  });

  Composite.add(world, ball);
}

// Drop a new ball every 500ms
setInterval(dropBall, 500);

5. Maintain Performance and Clean Up

Simulating hundreds of objects can cause frame drops if fallen balls accumulate indefinitely.