Simulate Momentum-Preserving Portals in Matter.js
This article explains how to build a portal teleportation mechanic in the Matter.js 2D physics engine that accurately preserves a rigid body’s speed and relative trajectory. By leveraging collision sensors, rotation mathematics, and direct velocity manipulation, you will learn how to transfer a body from an entry portal to an exit portal while seamlessly redirecting its linear momentum according to the portals' orientations.
Understanding Portal Mechanics
A portal pair consists of an entry portal and an exit portal, each defined with a position and an angle of orientation. When a dynamic body enters a portal, three operations must take place within a single simulation step:
- Repositioning: Translate the body's coordinates to the exit portal, placing it far enough outward along the exit normal to prevent infinite teleportation loops.
- Angle Transformation: Rotate the body's orientation to match the rotation delta between the two portals.
- Momentum Redirection: Rotate the linear velocity vector of the body by the difference in orientation between the entry and exit portals so that momentum feels continuous.
Setting Up Portal Sensors
Portals must detect collisions without physically deflecting the
traveling bodies. In Matter.js, configure portal bodies as sensors using
the isSensor: true flag.
const portalA = Matter.Bodies.rectangle(200, 500, 20, 100, {
isStatic: true,
isSensor: true,
angle: 0, // facing right (normal along +X)
label: 'portal_A'
});
const portalB = Matter.Bodies.rectangle(600, 200, 20, 100, {
isStatic: true,
isSensor: true,
angle: Math.PI / 2, // facing down (normal along +Y)
label: 'portal_B'
});
// Link portals via references
portalA.targetPortal = portalB;
portalB.targetPortal = portalA;
Matter.Composite.add(engine.world, [portalA, portalB]);Calculating Velocity and Position Offsets
To preserve momentum, compute the relative angle delta (\(\Delta\theta\)) between the two portals. Because exiting a portal involves traveling outward rather than inward, add \(\pi\) (180 degrees) to invert the entry direction:
\[\Delta\theta = \theta_{\text{exit}} - \theta_{\text{entry}} + \pi\]
Transform the velocity vector using standard 2D vector rotation:
\[v'_x = v_x \cos(\Delta\theta) - v_y \sin(\Delta\theta)\] \[v'_y = v_x \sin(\Delta\theta) + v_y \cos(\Delta\theta)\]
Implementing the Collision Listener
Listen for collisionStart events on the engine. When a
dynamic body touches a portal, calculate the new state and apply it
immediately using Matter.Body.setPosition,
Matter.Body.setAngle, and
Matter.Body.setVelocity.
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
let body = null;
let portal = null;
if (pair.bodyA.targetPortal && !pair.bodyB.isStatic) {
portal = pair.bodyA;
body = pair.bodyB;
} else if (pair.bodyB.targetPortal && !pair.bodyA.isStatic) {
portal = pair.bodyB;
body = pair.bodyA;
}
if (body && portal) {
teleportBody(body, portal, portal.targetPortal);
}
});
});
function teleportBody(body, entry, exit) {
// 1. Calculate rotation delta
const deltaAngle = exit.angle - entry.angle + Math.PI;
// 2. Rotate current velocity to preserve momentum
const currentVelocity = body.velocity;
const newVelocity = Matter.Vector.rotate(currentVelocity, deltaAngle);
// 3. Compute exit position with a forward safety offset
const exitNormal = {
x: Math.cos(exit.angle),
y: Math.sin(exit.angle)
};
const spawnDistance = 25; // Exceeds body radius to prevent re-triggering
const newPosition = {
x: exit.position.x + exitNormal.x * spawnDistance,
y: exit.position.y + exitNormal.y * spawnDistance
};
// 4. Update body transform and dynamics
Matter.Body.setPosition(body, newPosition);
Matter.Body.setAngle(body, body.angle + deltaAngle);
Matter.Body.setVelocity(body, newVelocity);
}Avoiding Infinite Teleportation Loops
When the body arrives at the exit portal, it may trigger an immediate reverse teleportation if the exit position overlaps the exit portal's sensor. Mitigate this by:
- Offset Distance: Ensuring the
spawnDistancefully places the body outside the exit portal's sensor boundary. - Cooldown Timers: Storing a timestamp on the
teleported body (
body.lastTeleport = Date.now()) and ignoring portal collisions for a small window, such as 100 milliseconds.