If the server is responsible for running the matter.js engine, it's likely unnecessary and prohibitively expensive to send matter.js objects from the server to clients.
One possible design is for the server to emit a minimum amount of serialized information needed by the clients to render the game state on each tick. This is application-specific, but probably boils down to picking vertices from your mjs bodies and informing the clients of player positions, moves, scores, etc.
Once the client receives state, they are responsible for rendering it. This could be done with matter.js, canvas, p5.js, plain HTML or anything else. Clients are also responsible for reporting game-relevant mouse and keyboard actions to the server to be used by the game engine logic.
Here's a minimal, complete example of how this might work:
package.json:
{
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.17.2",
"matter-js": "^0.18.0",
"socket.io": "^4.4.1"
},
"engines": {
"node": "16.x"
}
}
server.js:
const express = require("express");
const Matter = require("matter-js");
const app = express();
const server = require("http").createServer(app);
const io = require("socket.io")(server);
app.use(express.static("public"));
const frameRate = 1000 / 30;
const canvas = {width: 300, height: 200};
const boxes = 20;
const boxSize = 20;
const wallThickness = 20;
const entities = {
boxes: [...Array(boxes)].map(() =>
Matter.Bodies.rectangle(
Math.random() * canvas.width,
boxSize,
Math.random() * boxSize + boxSize,
Math.random() * boxSize + boxSize,
)
),
walls: [
Matter.Bodies.rectangle(
canvas.width / 2, 0,
canvas.width,
wallThickness,
{isStatic: true}
),
Matter.Bodies.rectangle(
0, canvas.height / 2,
wallThickness,
canvas.height,
{isStatic: true}
),
Matter.Bodies.rectangle(
canvas.width,
canvas.width / 2,
wallThickness,
canvas.width,
{isStatic: true}
),
Matter.Bodies.rectangle(
canvas.width / 2,
canvas.height,
canvas.width,
wallThickness,
{isStatic: true}
),
]
};
const engine = Matter.Engine.create();
Matter.Composite.add(engine.world, Object.values(entities).flat());
const toVertices = e => e.vertices.map(({x, y}) => ({x, y}));
setInterval(() => {
Matter.Engine.update(engine, frameRate);
io.emit("update state", {
boxes: entities.boxes.map(toVertices),
walls: entities.walls.map(toVertices),
});
}, frameRate);
io.on("connection", socket => {
socket.on("register", cb => cb({canvas}));
socket.on("player click", coordinates => {
entities.boxes.forEach(box => {
// servers://stackoverflow.com/a/50472656/6243352
const force = 0.01;
const deltaVector = Matter.Vector.sub(box.position, coordinates);
const normalizedDelta = Matter.Vector.normalise(deltaVector);
const forceVector = Matter.Vector.mult(normalizedDelta, force);
Matter.Body.applyForce(box, box.position, forceVector);
});
});
});
server.listen(process.env.PORT, () =>
console.log("server listening on " + process.env.PORT)
);
public/index.html:
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.1/socket.io.js"></script>
<script>
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
const socket = io();
const draw = (body, ctx) => {
ctx.beginPath();
body.forEach(e => ctx.lineTo(e.x, e.y));
ctx.closePath();
ctx.fill();
ctx.stroke();
};
socket.once("connect", () => {
socket.emit("register", res => {
canvas.width = res.canvas.width;
canvas.height = res.canvas.height;
});
});
socket.on("update state", ({boxes, walls}) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#111";
ctx.strokeStyle = "#111";
walls.forEach(wall => draw(wall, ctx));
ctx.fillStyle = "#aaa";
boxes.forEach(box => draw(box, ctx));
});
document.addEventListener("mousedown", e => {
socket.emit("player click", {x: e.offsetX, y: e.offsetY});
});
</script>
</body>
Live demo and project code are on glitch.
Other approaches exist and there can be advantages to shifting some of the engine logic to clients, so consider this a proof of concept. See this post for additional design suggestions.