<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Matter.js Top-Down Player Demo</title>
<style>
/* Basic styling to center the canvas and remove scrollbars */
body {
margin: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #1a1a1a;
}
canvas {
display: block;
background-color: #2c2c2c;
}
.instructions {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-family: Arial, sans-serif;
font-size: 16px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
pointer-events: none; /* Make it non-interactive */
}
</style>
</head>
<body>
<div class="instructions">Use WASD to move</div>
<!-- The Matter.js library is loaded from a CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<script>
function createRectWithOrigin(
x,
y,
width,
height,
origin = { x: 0.5, y: 0.5 },
options = {}
) {
// Calculate the center offset from the desired origin
const offsetX = (0.5 - origin.x) * width;
const offsetY = (0.5 - origin.y) * height;
// Create the rectangle at the world position plus the offset
const rect = Bodies.rectangle(
x + offsetX,
y + offsetY,
width,
height,
options
);
// Shift vertices so the physics center aligns with the origin
//Body.translate(rect, Vector.create(-offsetX, -offsetY));
return rect;
}
/**
* Create thin rectangles along a line path to approximate a line for collisions.
* @param {Vector[]} points - Array of points [{x, y}, ...] defining the line path
* @param {number} thickness - The thickness of the rectangles
* @param {object} options - Optional Matter.js body options
* @param {World} world - Matter.World to add the rectangles to
* @returns {Body[]} - Array of rectangle bodies created
*/
function createLineBodies(
x,
y,
points,
thickness = 2,
options = {},
world
) {
const bodies = [];
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[i + 1];
// Compute segment vector
const delta = Vector.sub(p2, p1);
const length = Vector.magnitude(delta);
// Compute angle
const angle = Math.atan2(delta.y, delta.x);
const wallOptions = {
...options,
isStatic: true,
friction: 0.01, // Low friction to encourage sliding
render: {
fillStyle: "#a1a1a1",
},
};
// Create thin rectangle for this segment
const rect = Bodies.rectangle(
x + (p1.x + p2.x) / 2,
y + (p1.y + p2.y) / 2,
length,
thickness,
Object.assign({}, wallOptions, { angle })
);
bodies.push(rect);
if (world) World.add(world, rect);
}
return bodies;
}
/**
* Create a compound Matter.js body from pre-decomposed convex polygons
* @param {number} x - world x position of the origin
* @param {number} y - world y position of the origin
* @param {Array[]} convexParts - array of convex vertex sets, e.g. [[{x,y},...], ...]
* @param {object} options - Matter.js body options applied to all parts
* @param {Vector} origin - optional offset vector to shift origin (default {x:0,y:0})
* @param {World} world - optional Matter.World to add the body to
* @returns {Body} compound Matter.js body
*/
function createCompoundBody(
x,
y,
convexParts,
options = {},
origin = { x: 0, y: 0 },
world
) {
const parts = convexParts.map((vertices) => {
// Create each convex body at origin; options can include render or physics settings
return Bodies.fromVertices(x, y, [vertices], options, true);
});
// Combine all parts into a single compound body
const compound = Body.create({
parts: parts,
...options,
});
// Adjust origin if requested
if (origin.x !== 0 || origin.y !== 0) {
Body.translate(compound, { x: -origin.x, y: -origin.y });
}
return compound;
}
// --- MODULE ALIASES ---
// These aliases make the code easier to read
const { Engine, Render, Runner, World, Bodies, Body, Events, Vector } =
Matter;
// --- SCENE SETUP ---
const sceneWidth = 800;
const sceneHeight = 600;
// 1. Create the physics engine
const engine = Engine.create();
// Disable gravity for a top-down view
engine.world.gravity.y = 0;
// 2. Create the renderer
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: sceneWidth,
height: sceneHeight,
wireframes: false, // Set to false for solid colors
background: "#333",
showPerformance: true,
},
});
// 3. Create a runner to step the engine forward
// --- PLAYER SETUP ---
const playerRadius = 15;
const player = Bodies.circle(
sceneWidth / 2,
sceneHeight / 2,
playerRadius,
{
label: "player",
// Physics properties for a "slidey" feel with acceleration
frictionAir: 0.08, // Increased air friction for a smoother stop
friction: 0.0, // Very low friction against other bodies
restitution: 0.0, // A little bounciness
inertia: Infinity, // Prevents the player from rotating on collision
render: {
fillStyle: "#4287f5", // Player color
},
}
);
// --- CHASER SETUP ---
const chasers = [];
const chaserCount = 0;
const chaserRadius = 14;
const chaserForce = 0.000511;
const chaserOptions = {
label: "chaser",
frictionAir: 0.07,
friction: 0.0,
density: 0.00051,
restitution: 0.0, // Make them a bit bouncy
render: {
fillStyle: "#f55a42", // Chaser color
},
};
for (let i = 0; i < chaserCount; i++) {
// Spawn them in random locations away from the center
const x = Math.random() * sceneWidth;
const y = Math.random() * sceneHeight;
const chaser = Bodies.circle(x, y, chaserRadius, chaserOptions);
chasers.push(chaser);
}
// --- WALLS & OBSTACLES ---
// The walls are static, meaning they don't move
const wallOptions = {
isStatic: true,
friction: 0.01, // Low friction to encourage sliding
render: {
fillStyle: "#a1a1a1",
},
};
const wallThickness = 50;
const walls = [
...createLineBodies(200, 100, [
{ x: 0, y: 0 },
{ x: 500, y: 5 },
{ x: 500, y: 30 },
{ x: 0, y: 35 },
{ x: 0, y: 200 },
{ x: 100, y: 200 },
{ x: 400, y: 210 },
]), // Top
Bodies.rectangle(sceneWidth / 2, 0, sceneWidth, wallThickness, {
...wallOptions,
}), // Top
// Outer boundaries
Bodies.rectangle(
sceneWidth / 2,
sceneHeight,
sceneWidth,
wallThickness,
{ ...wallOptions }
), // Bottom
Bodies.rectangle(0, sceneHeight / 2, wallThickness, sceneHeight, {
...wallOptions,
}), // Left
Bodies.rectangle(
sceneWidth,
sceneHeight / 2,
wallThickness,
sceneHeight,
{ ...wallOptions }
), // Right
];
// --- CORNER OBSTACLES ---
// These are made of two static rectangles to test multi-body collisions
const cornerSize = 150;
const cornerThickness = 20;
// Top-left corner
const topLeftCorner = [
createRectWithOrigin(
100,
wallThickness / 2,
10,
500,
{ x: 0.5, y: 0 },
{
...wallOptions,
render: {
fillStyle: "#9217f5",
},
}
),
];
// Bottom-right corner
const bottomRightCorner = [
Bodies.rectangle(
sceneWidth - cornerSize / 2,
sceneHeight - 100 - cornerThickness / 2,
cornerSize,
cornerThickness,
{ ...wallOptions }
),
Bodies.rectangle(
sceneWidth - cornerSize + cornerThickness / 2,
sceneHeight - 100 - cornerSize / 2,
cornerThickness,
cornerSize,
{ ...wallOptions }
),
];
World.add(engine.world, [
player,
...walls,
...topLeftCorner,
...bottomRightCorner,
...chasers, // Add this line
]);
const keys = {};
const playerForce = 0.0015; // A small force value for gradual acceleration
window.addEventListener("keydown", (event) => {
keys[event.code] = true;
});
window.addEventListener("keyup", (event) => {
keys[event.code] = false;
});
// This event runs just before the engine updates each frame
Events.on(engine, "beforeUpdate", (event) => {
const force = Vector.create(0, 0);
if (keys["KeyW"]) {
force.y -= playerForce;
}
if (keys["KeyS"]) {
force.y += playerForce;
}
if (keys["KeyA"]) {
force.x -= playerForce;
}
if (keys["KeyD"]) {
force.x += playerForce;
}
// Apply the calculated force to the player's center
Body.applyForce(player, player.position, force);
chasers.forEach((chaser) => {
// Calculate vector from chaser to player
const direction = Vector.sub(player.position, chaser.position);
// Normalize the vector (get a unit vector)
const normalizedDirection = Vector.normalise(direction);
// Create a force vector and apply it to the chaser
const force = Vector.mult(normalizedDirection, chaserForce);
Body.applyForce(chaser, chaser.position, force);
});
});
const physicsDelta = 1000 / 600;
let lastTime = performance.now();
let accumulator = 0;
// This function will be our main game loop.
function gameLoop(currentTime) {
debugger;
// Calculate how much time has passed since the last frame.
const elapsed = currentTime - lastTime;
lastTime = currentTime;
// Add the elapsed time to an accumulator.
accumulator += elapsed;
// Trigger the 'beforeUpdate' event where all your movement logic lives.
// The default runner does this automatically, so we must do it manually here.
Events.trigger(engine, "beforeUpdate", {
timestamp: engine.timing.timestamp,
});
// While the accumulator has enough time for one or more physics steps,
// update the engine. This loop ensures the physics simulation "catches up"
// if rendering falls behind, maintaining a consistent simulation speed.
while (accumulator >= physicsDelta) {
// Update the engine by a fixed amount.
Engine.update(engine, physicsDelta);
accumulator -= physicsDelta;
}
// Render the current state of the world.
// This is done once per frame, regardless of how many physics steps were taken.
Render.world(render);
// Request the next animation frame from the browser to continue the loop.
requestAnimationFrame(gameLoop);
}
// Start the custom game loop!
requestAnimationFrame(gameLoop);
</script>
</body>
</html>