Screen Wrapping Boundaries in Matter.js

This article explains how to implement seamless screen wrapping boundaries in Matter.js physics simulations. Instead of constraining dynamic bodies with static barrier walls, you will learn how to detect when a body exits the viewport and instantly reposition it on the opposite edge. By tracking physics update events and applying manual coordinate adjustments using built-in Matter.js methods, you can easily build classic wrap-around mechanics suitable for arcade-style games like Asteroids.

The Screen Wrapping Concept

In a standard physics engine setup, boundaries are typically created by placing thick, static rectangular bodies along the edges of the canvas. Screen wrapping—often referred to as toroidal space—removes these static walls. Instead, the simulation monitors each body's coordinates during every engine tick. When an object crosses an outer threshold, its position is shifted across the canvas while maintaining its current linear and angular velocities.

Listening to Engine Update Events

To check body positions continuously, hook into the beforeUpdate or afterUpdate event of your Matter.js engine. The beforeUpdate event is generally preferred because adjusting positions before physics calculations run prevents visual jittering and collision glitches.

Use Matter.Events.on to attach a callback function to your engine instance:

const { Events, Body, Composite } = Matter;

Events.on(engine, 'beforeUpdate', () => {
    // Wrapping logic runs here
});

Writing the Wrapping Logic

When repositioning bodies, account for their dimensions using an offset (or padding) based on their bounding box. If an object is repositioned the exact moment its center reaches an edge, the object will noticeably pop out of view. Using an offset ensures the body fully leaves one side before appearing on the other.

Use Matter.Body.setPosition rather than directly mutating body.position.x or body.position.y. Direct mutation can break the engine's internal velocity and previous-position caches.

Here is the implementation:

const width = 800;
const height = 600;

Events.on(engine, 'beforeUpdate', () => {
    const bodies = Composite.allBodies(engine.world);

    bodies.forEach((body) => {
        // Skip static bodies such as scenery or anchors
        if (body.isStatic) return;

        // Calculate body dimensions to prevent visual popping
        const bodyWidth = body.bounds.max.x - body.bounds.min.x;
        const bodyHeight = body.bounds.max.y - body.bounds.min.y;
        const halfWidth = bodyWidth / 2;
        const halfHeight = bodyHeight / 2;

        let newX = body.position.x;
        let newY = body.position.y;
        let wrapped = false;

        // Horizontal wrapping
        if (body.position.x > width + halfWidth) {
            newX = -halfWidth;
            wrapped = true;
        } else if (body.position.x < -halfWidth) {
            newX = width + halfWidth;
            wrapped = true;
        }

        // Vertical wrapping
        if (body.position.y > height + halfHeight) {
            newY = -halfHeight;
            wrapped = true;
        } else if (body.position.y < -halfHeight) {
            newY = height + halfHeight;
            wrapped = true;
        }

        // Apply updated position safely
        if (wrapped) {
            Body.setPosition(body, { x: newX, y: newY });
        }
    });
});

Best Practices