Running Matter.js on a Node server

I'm trying to run a server that runs a physics simulation on a server and have clients connect to this server via websockets/socket.io. I know I can calculate the Engine separately from the rendering with Matter.js. So my question is, how do I get the engine data to the client?

I have a Game class, and on socket connection, I want to emit the World to the client to render.

var g = new Game()
g.initEngine()

io.sockets.on('connection', function(socket) {
   io.emit('gamestate', g.getGameState())
});

In the game state, I'm not really sure what to pass:

var Game = function(Player1, Player2) {
    var self = this
    this.gameID = 22
    this.engine = null
    this.world = null
    
    // Get game state - WHAT DO I SEND HERE!??
    this.getGameState = function() {
      return self.engine
    }

   // Create the engine
   this.initEngine = function() {
    self.engine = M.Engine.create();
    self.world = self.engine.world;
    self.world.gravity.x = 0;
    self.world.gravity.y = 0;
    M.Engine.update(self.engine, 122000 / 60);
  }
}

When I try pass self, or self.engine, or self.world, I just crash the app. It says Maximum call stack size exceeded.

What data do I need to extract from self.engine to send to the client neatly over WebSockets?

I know I need position data of the bodies. But even when I try

return self.engine.world.bodies;

it crashes then as well.

How can I simply get the engine/world to the clients without crashing or exceeding stack size?


Solution 1:

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.World.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.