Build a Tilt Maze with Matter.js and Accelerometer

Simulating a classic rolling ball tilt maze on a mobile device involves pairing Matter.js, a 2D physics engine, with the smartphone's built-in motion sensors. This guide explains how to construct the physical environment—including the maze walls and ball—capture real-time tilt data via the DeviceOrientationEvent API, and map those sensor inputs directly to Matter.js's gravity vector to drive the simulation smoothly.

1. Setting Up the Matter.js Environment

Begin by initializing the core Matter.js modules: Engine, Render, Runner, Bodies, and Composite. The simulation requires an engine to compute the physics and a renderer to draw the canvas on the screen.

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

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

// Disable standard downward gravity initially
engine.gravity.x = 0;
engine.gravity.y = 0;

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

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

2. Creating the Maze and the Ball

The maze consists of static rectangular bodies, while the ball is a dynamic circle that responds to physics.

// Create the rolling ball
const ball = Bodies.circle(100, 100, 20, {
  restitution: 0.3, // Slight bounciness
  friction: 0.05,
  render: { fillStyle: '#e74c3c' }
});

// Create boundary walls
const wallOptions = { isStatic: true, render: { fillStyle: '#2c3e50' } };
const thickness = 40;
const width = window.innerWidth;
const height = window.innerHeight;

const walls = [
  Bodies.rectangle(width / 2, 0, width, thickness, wallOptions),
  Bodies.rectangle(width / 2, height, width, thickness, wallOptions),
  Bodies.rectangle(0, height / 2, thickness, height, wallOptions),
  Bodies.rectangle(width, height / 2, thickness, height, wallOptions),
  // Internal maze walls
  Bodies.rectangle(width / 2, height / 3, width * 0.6, 20, wallOptions)
];

Composite.add(world, [ball, ...walls]);

3. Capturing Smartphone Orientation

Smartphones expose physical tilt angles through the deviceorientation event:

Modern iOS versions (iOS 13+) require explicit user permission to read sensor data, typically triggered by a button press.

function requestMotionPermission() {
  if (typeof DeviceOrientationEvent.requestPermission === 'function') {
    DeviceOrientationEvent.requestPermission()
      .then(response => {
        if (response === 'granted') {
          window.addEventListener('deviceorientation', handleOrientation);
        }
      })
      .catch(console.error);
  } else {
    // Non-iOS 13+ devices
    window.addEventListener('deviceorientation', handleOrientation);
  }
}

4. Translating Tilt into Matter.js Gravity

Matter.js controls world gravity via engine.gravity.x, engine.gravity.y, and engine.gravity.scale. Instead of applying continuous forces to the ball, altering the world's gravity vector creates a more natural tilt sensation for all dynamic bodies inside the maze.

Map the gamma and beta values to the range [-1, 1], clamping them to prevent excessive acceleration when tilting the phone steeply:

const MAX_TILT = 30; // Degrees considered as maximum gravity input

function handleOrientation(event) {
  let x = event.gamma || 0; // Left/Right
  let y = event.beta || 0;  // Front/Back

  // Clamp the tilt values
  x = Math.max(-MAX_TILT, Math.min(MAX_TILT, x));
  y = Math.max(-MAX_TILT, Math.min(MAX_TILT, y));

  // Normalize between -1 and 1
  const gravityX = x / MAX_TILT;
  const gravityY = y / MAX_TILT;

  // Apply to the Matter.js physics world
  engine.gravity.x = gravityX;
  engine.gravity.y = gravityY;
  engine.gravity.scale = 0.001; // Adjust for realistic rolling speed
}

5. Handling Display and Calibration Adjustments

To ensure a seamless gameplay experience:

  1. Lock Screen Orientation: Lock the display orientation using the Screen Orientation API (screen.orientation.lock('portrait')) if supported, preventing unwanted viewport recalculations while tilting.
  2. Neutral Tilt Calibration: Many users hold their devices at a 30°–45° incline naturally. Calibrate the baseline by recording the initial beta angle when the game starts and subtracting it from subsequent readings:
    let baselineBeta = 30; // Default holding angle
    const calibratedY = y - baselineBeta;
  3. Friction and Mass Adjustments: Increase the ball’s frictionAir (e.g., 0.02) on the Matter.js body to prevent the ball from sliding excessively and to simulate the rolling resistance of a real sphere.