matter-js icon indicating copy to clipboard operation
matter-js copied to clipboard

This is how to create a multiplayer game with matter-js (synchronize state)

Open Kibergx opened this issue 2 years ago • 1 comments

Since I couldn't find any specific solution I created one for myself. It's really hard to figure out for a beginner how to create a multiplayer game with this engine, so here are my experiences.

The idea is that the server sending the keyboard inputs to all the clients. Every client calculates the exact same physics. In theory it is not a big deal, you just need fixed timestep updates, and updates at the exact same rate. But, if not all your players connect at the same time it is not that easy, you need to synchronize the game state with the new player (or you need to execute all the server input updates which happened in the past). This synchronization gave me huge headackes, but finally it seems working fine.

I parsed my whole Matter.Physics instance, and my own game state. But this is not that simple because of the circular references, and because JSON does not support NaN, Infinity, undefined. (You can't just send information about the bodies in the scene, because many other things cached in the engine, look Matter.Pairs, Matter.detector).

1, Firstly I used this replaceNaNAndInfinity function, to change the non-json types for same special string, so later I can parse it properly. Because of the circular references I am using a depth limit.

export function replaceNaNAndInfinity(obj: any, keyStart="", depth=0): any{
    if(depth>5){
        return;
    }

    //console.log(keyStart)
    // Base case: if obj is a number, check if it's NaN or Infinity
    if(obj==undefined){
        return "--undefined";
    }

    if (typeof obj === "number") {
      if (Number.isNaN(obj)) {
        return "--NaN";
      } else if (obj === Infinity) {
        return "--Infinity";
      } else if (obj === -Infinity) {
        return "---Infinity";
      } else {
        return obj;
      }
    }
    
    // Recursive case: if obj is an object or array, recurse on its keys/values
    if (typeof obj === "object" && obj !== null) {
      if (Array.isArray(obj)) {
        return obj.map( (val, index) => replaceNaNAndInfinity(val, keyStart+"[" + index + "]", depth+1));
      } else {
        for (const [key, val] of Object.entries(obj)) {
          const result=replaceNaNAndInfinity(val, keyStart+"."+key, depth+1);

          if(typeof result=="string"){
            if(result=="--NaN"){
                obj[key]="--NaN";
            }
            if(result=="--Infinity"){
                obj[key]="--Infinity";
            }
            if(result=="---Infinity"){
                obj[key]="---Infinity";
            }
            if(result=="--undefined"){
                obj[key]="--undefined";
            }
          }

        }
        return obj;
      }
    }
    
    // Base case: if obj is not a number or object/array, return it unchanged
    return obj;
}

2, With the flatted (https://github.com/WebReflection/flatted) lib I stringify the object from the results of the replaceNaNAndInfinity function. This lib handles circular references. I send the game state to the server.

3, The game state string arrived. I parse it with the flatted lib.

4, I replace the special strings like "--Infinity" with the real values.

export function replaceStringNaNAndInfinity(obj: any, depth=0): any {
    if(depth>10){
        return;
    }        
    // Base case: if obj is a string, check if it's "NaN" or "Infinity"
    if (typeof obj === "string") {
      if (obj === "--NaN") {
        return NaN;
      } else if (obj === "--Infinity") {
        return Infinity;
      } else if (obj === "---Infinity") {
        return -Infinity;
      } else if (obj == "--undefined"){
        return "--undefined";
      } else {
        return obj;
      }
    }
    
    // Recursive case: if obj is an object or array, recurse on its keys/values
    if (typeof obj === "object" && obj !== null) {
      if (Array.isArray(obj)) {
        return obj.map(val => replaceStringNaNAndInfinity(val, depth+1));
      } else {            
        for (const [key, val] of Object.entries(obj)) {
          const result = replaceStringNaNAndInfinity(val, depth+1);

          if (typeof result === "number") {
            if(Number.isNaN(result)){
                obj[key]=NaN;
            } else if (result==Infinity){
                obj[key]=Infinity;
            } else if (result==-Infinity){
                obj[key]=-Infinity;
            }
          }

          if(typeof result=="string" && result=="--undefined"){
            obj[key]=undefined;
          }
        }
        return obj;
      }
    }
    
    // Base case: if obj is not a string or object/array, return it unchanged
    return obj;
}

5, I am not sure if this is neccesary, but I also delete the null valued array elements, because in the original Matter.Physics these are not null values, they don't exists.

export function removeNullElements(obj: any, depth=0) {
    if(depth>5){
        return;
    }

    if (typeof obj === "object" && obj !== null) {
        if (Array.isArray(obj)) {
            for(let i=0; i<obj.length; i++){
                if(obj[i]==null){
                    delete obj[i];
                }
            }
        } else {
            for (const [key, val] of Object.entries(obj)) {
                removeNullElements(val, depth+1);
            }
        }
    }
  }

After this, if all the clients doing the exact same things with the Matter lib I get the exact same results. If you need to use Date.now() for example jump delay for players, you can't do that. You need to use a server time, so every client working with the same data.

For browser compatibility you also need trigfills, as explained here: https://github.com/liabru/matter-js/issues/1174

Kibergx avatar May 01 '23 12:05 Kibergx

Could you please give your source code for sharing client/server state? It seems unoptimized to just serialize every object into JSON and send it over WS.

GulgDev avatar Dec 26 '24 19:12 GulgDev