Matter.js 2D Platformer Character Controller
Building a responsive platformer character controller using Matter.js requires balancing rigid-body physics with predictable, arcade-style movement. This guide explains how to configure a character body, prevent unwanted tipping, set up a ground-check sensor, handle horizontal movement with friction mitigation, and implement a consistent jump mechanic.
1. Creating the Character Body
Standard rigid bodies in physics engines tend to rotate and snag on terrain edges. To solve this, create a composite body consisting of a main body with locked rotation and slightly rounded edges (or a chamfered rectangle), paired with a sensor at the bottom to detect the floor.
const { Bodies, Body } = Matter;
const width = 40;
const height = 60;
const x = 200;
const y = 200;
// Main collision body
const playerBody = Bodies.rectangle(x, y, width, height, {
chamfer: { radius: 10 },
friction: 0.001,
frictionAir: 0.05,
render: { fillStyle: '#2ecc71' }
});
// Ground detection sensor placed at the player's feet
const sensorHeight = 6;
const groundSensor = Bodies.rectangle(x, y + height / 2, width * 0.8, sensorHeight, {
isSensor: true,
render: { visible: false }
});
// Compound body
const player = Body.create({
parts: [playerBody, groundSensor],
inertia: Infinity // Locks rotation to keep the player upright
});Setting inertia: Infinity guarantees the character
remains strictly vertical regardless of impacts.
2. Detecting Ground Contacts
To prevent infinite jumping in mid-air, track collisions specifically
between the groundSensor and solid terrain bodies.
let isGrounded = false;
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
if (pair.bodyA === groundSensor || pair.bodyB === groundSensor) {
isGrounded = true;
}
});
});
Matter.Events.on(engine, 'collisionEnd', (event) => {
event.pairs.forEach((pair) => {
if (pair.bodyA === groundSensor || pair.bodyB === groundSensor) {
isGrounded = false;
}
});
});3. Implementing Horizontal Movement
Applying raw forces (Body.applyForce) can lead to a
floaty feeling. For snappy platformer controls, directly manipulate the
horizontal velocity while preserving the vertical physics generated by
gravity.
const moveSpeed = 5;
const keys = { left: false, right: false };
window.addEventListener('keydown', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if ((e.code === 'Space' || e.code === 'ArrowUp') && isGrounded) {
jump();
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
});
function updateMovement() {
let targetVelocityX = 0;
if (keys.left) targetVelocityX -= moveSpeed;
if (keys.right) targetVelocityX += moveSpeed;
Matter.Body.setVelocity(player, {
x: targetVelocityX,
y: player.velocity.y
});
}4. Executing the Jump
When the jump command triggers, override the vertical velocity directly to deliver an instantaneous, fixed-height upward burst.
const jumpForce = 12;
function jump() {
Matter.Body.setVelocity(player, {
x: player.velocity.x,
y: -jumpForce
});
}Call updateMovement() inside the engine's
beforeUpdate event loop:
Matter.Events.on(engine, 'beforeUpdate', () => {
updateMovement();
});5. Overcoming Common Platformer Issues
- Wall Sticking: If the player pushes against a
vertical wall while falling, standard friction can slow or pause
descent. Ensure the player's lateral friction is minimal
(
0.001or0). - Tile Edge Snagging: When running across floors built from multiple grid tiles, sharp corners can catch. Using a chamfered rectangle or a capsule collider prevents the character's base from snagging on internal tile seams.
- Variable Jump Heights: To allow short and high
jumps based on button-press duration, multiply
player.velocity.yby a damping factor (e.g.,0.5) if the jump key is released while the character is still moving upward.