How to Check if a Body is Grounded in Matter.js
Determining whether a character is touching the ground is a fundamental requirement for 2D platformers and physics-based character controllers. In Matter.js, detecting ground contact can be handled through collision events, normal vector analysis, or dedicated sensor bodies. This article covers the most reliable methods to detect ground contact, focusing primarily on the compound sensor body pattern and collision normal checks to prevent unwanted behaviors like wall jumping.
Method 1: Using a Foot Sensor (Recommended)
The most robust technique for character controllers is attaching a non-physical "sensor" body to the bottom of your character's main physics body. Sensors fire collision events without applying physical forces or causing unwanted friction.
1. Create a Compound Body with a Sensor
const { Bodies, Body } = Matter;
const width = 40;
const height = 60;
const sensorHeight = 10;
// Main character body
const mainBody = Bodies.rectangle(x, y, width, height, {
inertia: Infinity // Prevents character from tipping over
});
// Foot sensor positioned at the bottom of the main body
const sensor = Bodies.rectangle(x, y + height / 2, width * 0.8, sensorHeight, {
isSensor: true,
label: 'playerSensor'
});
// Combine into a compound body
const player = Body.create({
parts: [mainBody, sensor],
friction: 0.05
});2. Track Ground State via Collision Events
Maintain an integer counter for active ground contacts. Increment the
counter on collisionStart and decrement it on
collisionEnd. Using a counter instead of a boolean prevents
bugs when moving across multiple overlapping ground tiles.
let groundContactCount = 0;
Matter.Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((pair) => {
if (pair.bodyA.label === 'playerSensor' || pair.bodyB.label === 'playerSensor') {
groundContactCount++;
}
});
});
Matter.Events.on(engine, 'collisionEnd', (event) => {
event.pairs.forEach((pair) => {
if (pair.bodyA.label === 'playerSensor' || pair.bodyB.label === 'playerSensor') {
groundContactCount = Math.max(0, groundContactCount - 1);
}
});
});
// Check if grounded before jumping
function isGrounded() {
return groundContactCount > 0;
}Method 2: Evaluating Collision Normals
If you prefer not to use a compound body, you can analyze collision normal vectors during collision events on a single body. This allows you to differentiate between hitting a wall, ceiling, or floor.
let isGrounded = false;
Matter.Events.on(engine, 'collisionActive', (event) => {
let touchingGround = false;
event.pairs.forEach((pair) => {
// Ensure the player is involved in the collision
if (pair.bodyA === player || pair.bodyB === player) {
// Determine normal direction relative to the player
const normal = pair.collision.normal;
const isBodyA = pair.bodyA === player;
// If player is bodyA, a normal pointing up (negative Y) means ground
// If player is bodyB, the normal points in the opposite direction
const normalY = isBodyA ? normal.y : -normal.y;
// A normalY threshold of less than -0.5 confirms contact from below
if (normalY < -0.5) {
touchingGround = true;
}
}
});
isGrounded = touchingGround;
});Method 3: Direct Queries with Matter.Query
For immediate checks outside of the event loop, use
Matter.Query.ray or Matter.Query.collides.
This approach checks if an area directly beneath the character
intersects with other bodies right before applying a jump force.
function checkGroundDirectly(player, groundBodies) {
const bottomCenter = {
x: player.position.x,
y: player.bounds.max.y
};
const rayEnd = {
x: bottomCenter.x,
y: bottomCenter.y + 5 // Check 5 pixels below the body
};
const collisions = Matter.Query.ray(groundBodies, bottomCenter, rayEnd);
return collisions.length > 0;
}Summary of Best Practices
- Use slightly narrower sensors: Make the bottom
sensor slightly narrower than the character body (e.g.,
width * 0.8) to avoid registering walls as ground when sliding against vertical surfaces. - Use a contact counter: Collision pairs can fire at
different times across seams in the ground; a counter variable avoids
setting the grounded state to
falseprematurely. - Filter collisions: Use
collisionFiltercategories or custom labels to ensure background objects and triggers are ignored when evaluating ground contact.