Jump to content

Building a multiplayer physics game. Looking for insight. :)


Recommended Posts

Hi!

Thanks for joining me from my previous thread or if you're new here!

Previous thread: 

MAJOR GOAL: Create a multiplayer game using websockets. 

GAME CONCEPT: Attempt to be the last ball surviving as other balls and environmental effects attempt to knock you off the platform.

STATUS: Finessing the single-player version where it is just player vs. environmental effects.

Check out the current incarnation of the single-player game here: http://aaronjanke.com/ballGame/

Github: https://github.com/ballAndBoardInc/ballGame

CHALLENGE #1 Currently, I'm looking at finessing the controls. They accelerate too quickly, since holding down the key doesn't provide immediate repetition of the trigger. It's awkward. Looking for smoother acceleration.

CHALLENGE #2 We are implementing a game reset, but in its current incarnation it doesnt properly rebind the controls after it resets.

Definitely open to thoughts.

This is our current control system:

scene.actionManager = new BABYLON.ActionManager(scene);
 
scene.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
{
trigger: BABYLON.ActionManager.OnKeyDownTrigger,
parameter: 'a'
},
function () {
console.log('a pressed');
playerMesh.applyImpulse(new BABYLON.Vector3(10, 0, 0), playerMesh.getAbsolutePosition());
}
)
);
 
scene.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
{
trigger: BABYLON.ActionManager.OnKeyDownTrigger,
parameter: 'w'
},
function () {
console.log('w pressed');
playerMesh.applyImpulse(new BABYLON.Vector3(0, 0, -10), playerMesh.getAbsolutePosition());
}
)
);
scene.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
{
trigger: BABYLON.ActionManager.OnKeyDownTrigger,
parameter: 'd'
},
function () {
console.log('d pressed');
playerMesh.applyImpulse(new BABYLON.Vector3(-10, 0, 0), playerMesh.getAbsolutePosition());
}
)
);
scene.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
{
trigger: BABYLON.ActionManager.OnKeyDownTrigger,
parameter: 's'
},
function () {
console.log('s pressed');
playerMesh.applyImpulse(new BABYLON.Vector3(0, 0, 10), playerMesh.getAbsolutePosition());
}
)
);
Link to post
Share on other sites

Hi again, AJTJ!

   Just to tell people... previous thread:  http://www.html5gamedevs.com/topic/36762-how-do-i-control-a-ball-and-create-a-contained-game-world/

Over there, I suggested reading http://www.html5gamedevs.com/topic/36672-movewithcollision-issue-quirks/?tab=comments#comment-210347

That is where I show/talk-about the invisible joint method of p-mesh (physicsMesh) moving... which... was taught to me by someone else.  It seems like the player stays under control, better, with that system, though.  https://www.babylonjs-playground.com/#15AFCG#30  (#29 for a slightly slower-motion version)

The little gray ibox would be invisible... in the end.  The physicsJoint between the gray ibox and the green player box... is already invisible (it is an equation, and equations have no physical form)  :D

Keypress auto-repeating (held keys) probably needs to be pondered.  Physics forces can "accumulate".  Without mass and friction to counter-act accumulated forces, players can go a-flying.

Mass can be set dynamically (make player get heavier as it moves further, then reset to normal when stopped.  Like real-time adjustable brakes.).  When mass increases, so can friction against the ground.  You can change it on-the-fly, but I have not seen any applications that do that, yet. 

That "sharing a joint with higher powers"  ;) (#29 / #30 PG) can help with over-power, and its easier than doing dynamic mass adjustments.

We have what?  FOUR 3rd-party physics engines within "reach", now?  I think so.  OimoJS, AmmoJS, CannonJS, and EnergyJS.  Phew.  CannonJS probably has the best docs, so far.

Stay tuned... other comments coming.  For me, physics engines are like beautiful women that you fall in love-with, who repeatedly punch you in the head, and you still can't stop loving them.  :D

Link to post
Share on other sites

Lots to check in all of that. I'll be sure to explore it. Player controls are a very specific beast.

Here's another question:

What sort of gameplay elements would you consider cool?

I like the idea of building up mass as you accelerate, that would be great for the multiplayer element. Maybe having your color change/intensify as you accelerate/gain mass, that would be badass.

I'm also thinking of maybe including a "radial physics explosion power" that is built up based on some sort of factor, like gathering three glowing gems or something. This would also force interactivity to the players, because everyone would be attempting to grab the gems before someone else does.

I'm all about succinct gaming experiences.

AJTJ

Link to post
Share on other sites

For the fun of it, I tried recreating a multiplayer version. This is mostly copy/paste from various other projects of mine, so it's messsaah.

It is using native CannonJS, and you can't really push other players as their angularvelocity is zeroed when keys aren't pressed. Easy to fix, though.

It is FAR from perfect, but it has the basics. Authoritative server, input prediction, server reconciliation, entity interpolation and a fixed timestep input loop in case of lags. Pretty much anything could be optimized. Loops, meshes(instances) etc. But in case you need some ideas for how you Could handle networking, you can take a look at it. I have a 130ms ping or round trip latency, and it seems to run decently, although collisions do seem a bit off at times. Networking is hard :P

Btw. If you press "Run" in the playground, the socket connection isn't dropped, so you'll have multiple balls to control, with only the most recent being applied reconciliation and prediction, so it should represent how you see other players.

http://playground.babylonjs.com/#RM9TBC#1

It does seem to have some crashes that don't happen in the non-pg version, but you'll get the idea.

 


var CANNON = require('cannon'),
    express         = require('express'),
    WebSocket = require('ws'),
    gameloop = require('node-gameloop'),
    http = require('http');



var server = http.createServer(function(request, response) {

});
server.listen(7777, function() {


process.on('warning', e => console.warn(e.stack));
//console.log(id);
 });


var players = [], boxBodies = [];
var world;
var playerIDs = 1, bodyIDs = 1;
var bodiesToRemoveFromWorld = [];


var wss = new WebSocket.Server({ port: 8088 });

wss.on('connection', function connection(ws, req) {
   
    ws.isAlive = true;
   
    
    ws.on('pong', function(){
    
    ws.isAlive = true});    
    
    ws.id = playerIDs++;
  
  
    ws.on('message', function incoming(message) {
   
        translate(message, ws);  
    });
    
 ws.on('error', function close(e) {
  console.log('error', e);
   
  
    });   
    
    
  
 ws.on('close', function close() {
 console.log('disconnected');
     
  if(!ws.id){ 
    console.log("no ID");  
    return;
  }
  var player = findPlayerByID(ws.id);
     
        if(player){
            console.log("Player found");
         
            broadcastToAllSockets({m:17, p:ws.id});
            removePlayer(player);
        } 
      
      
    });
    
});


const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
        var player = findPlayerByID(ws.id);
     
        if(player){
            console.log("Player found");
         
            broadcastToAllSockets({m:17, p:ws.id});
            removePlayer(player);
        } 
        
        return ws.terminate()
    
    };

    ws.isAlive = false;
    //ws.pingStart = Date.now();  
    ws.ping('', false, true);
      
  });
}, 2000);



function removePlayer(player){
    if(!player){
        console.log("Not found");
        return;
    }
    
    bodiesToRemoveFromWorld.push(player.body);
    var index = players.indexOf(player);
    if(index > -1){
        players.splice(index, 1);
    }
}



function translate(message, socket){
    
    var decoded = JSON.parse(message);
    
    if(decoded.m === 0){
      
        requestGameState(socket);
        //var player = new Player();
        //player.id = socket.id = playerIDs++;
        //emitIfOpen(socket, {m:1, p:player.id});
    } else if(decoded.m === 2){
      
        console.log("Requesting players");
       // var player = new Player();
        //player.id = socket.id;
        //emitIfOpen(socket, {m:1, p:player.id});
        //broadcastToAllSockets({m:1, p:player.id});
    } else if(decoded.m === 4){
      
        
        var player = new Player();
        player.id = socket.id;
        player.socket = socket;
        player.hasSpawned = true;
        broadcastToAllSockets({m:1, p:player.id});
        emitIfOpen(socket, {m:1, p:player.id});
        players.push(player);
    } else if(decoded.m === 10){
        //console.log("m");
        applyInput(socket.id, decoded.p);
        
    }
    
    
  
  
    
}


function applyInput(id, input){
    
    var player = findPlayerByID(id);
    if(!player){
        console.log("nope");
        return;
    }
    
  
    
     if(input.w){
       
        player.body.angularVelocity.x = 10;
        //player.body.position.x += 0.1;
    } else if(input.s) {
       // player.body.position.x -= 0.1;
       player.body.angularVelocity.x = -10;
    } else {
        player.body.angularVelocity.x = 0;
    }
    
   
    if(input.a){
        player.body.angularVelocity.z = 10;
       // player.body.position.z -= 0.1;
    } else if(input.d) {
        player.body.angularVelocity.z = -10;
       // player.body.position.z += 0.1;
    } else {
        player.body.angularVelocity.z = 0;
    }
    
    
    
    
    player.lastProcessedInput = input.seq;
    
    
}


function requestGameState(socket){
    var states = [];
    for(var i=0;i<players.length;i++){
        var player = players[i];
        if(player){
            states.push([player.id, player.body.position.x, player.body.position.y, player.body.position.z]);
        }
    }
    emitIfOpen(socket, {m:4, p:states, i:socket.id});
    
}


function emitIfOpen(socket, packet){
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify(packet)); 
        
    }   
}



var Player = function(){
    
    this.mass = 5;
    this.radius = 0.5;
    this.pings = [];
    this.ping = 0;
 
    this.lastProcessedInput = 0;
  
    this.alive = false;
    this.hasSpawned = false;
  
   
    this.shape = new CANNON.Sphere(this.radius); 
   
    
    this.body = new CANNON.Body({mass: 1});
  
    this.body.fixedRotation = true;
    this.body.updateMassProperties();
    this.body.addShape(this.shape, new CANNON.Vec3(0,0,0));
    world.add(this.body);
    
    
    return this;
    
   
   
}




function findPlayerByID(id){
    
    for (var i=0;i<players.length;i++){
        
        if(players[i].id === id){
           // console.log("Player Found");
            return players[i];
            
        }
        
    }
    
}



var createWorld = function(){
        
     
            world = new CANNON.World();
  
      
            world.quatNormalizeSkip = 0;
            world.quatNormalizeFast = false;
            world.defaultContactMaterial.contactEquationStiffness = 1e128;
            world.defaultContactMaterial.contactEquationRelaxation = 4;
           // world.gravity.set(0, -9.82, 0);
            world.solver.iterations = 20;
            world.solver.tolerance = 0.0;

            world.gravity.set(0,-30,0);
            world.broadphase = new CANNON.NaiveBroadphase();
            var groundShape = new CANNON.Plane();
            var groundBody = new CANNON.Body({ mass: 0, shape: groundShape});
          //, collisionFilterGroup: 2, collisionFilterMask: 1
            groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1,0,0),-Math.PI/2);
      
            //world.add(groundBody);
      
            var boxShape01 = new CANNON.Box(new CANNON.Vec3(10,0.2,3));
            var boxShape02 = new CANNON.Box(new CANNON.Vec3(10,10,0.2));

           // x: -0.08715574274765817, y: -0, z: -0, w: 0.9961946980917455
      
      
            var boxBody = new CANNON.Body({ mass: 0});
            boxBody.addShape(boxShape01, new CANNON.Vec3(0,-1,0), new CANNON.Quaternion(-0.08715574274765817,0,0,0.9961946980917455));
            boxBody.addShape(boxShape02, new CANNON.Vec3(0,9,7), new CANNON.Quaternion(0.19509032201612825,0,0,0.9807852804032304));

            world.add(boxBody);
      
            
            for(var i=0;i<3;i++){
                var body = {cType:0, id:bodyIDs++};
                var boxShape = new CANNON.Box(new CANNON.Vec3(2*0.5,1*0.5,2*0.5));
                var boxBody = new CANNON.Body({ mass: 50});
                boxBody.addShape(boxShape);
                body.body = boxBody;
                world.add(boxBody);
                boxBodies.push(body);
            }
      
      
            for(var i=0;i<3;i++){
                var body = {cType:1, id:bodyIDs++};
                var sphereShape = new CANNON.Sphere(1);
                var sphereBody = new CANNON.Body({ mass: 50});
                sphereBody.addShape(sphereShape);
                body.body = sphereBody;
                world.add(sphereBody);
                boxBodies.push(body);
            }
            
 
    
            return world;
    
        
  }
      
      
  
  

  




var mainLoop = gameloop.setGameLoop(function(delta) {
    

if(world){
    
    for(var i=0,length=bodiesToRemoveFromWorld.length;i<length;i++){
        if(bodiesToRemoveFromWorld[i]){
            world.remove(bodiesToRemoveFromWorld[i]);
        }
        bodiesToRemoveFromWorld.splice(i,1);
    }
    
    
    for(var i=0;i<boxBodies.length;i++){
        var body = boxBodies[i];
        if(body){
            body.body.velocity.z = -3;
        }
    }
    
    //console.log("stp");
    world.step(1.0/60);   
}   

}, 1000 / 60);   


var networkLoop2 = gameloop.setGameLoop(function(delta) {
    
    sendStates(); 
    
    for(var i=0;i<boxBodies.length;i++){
        var body = boxBodies[i];
        if(body){
            if(body.body.position.y < -10){
                body.body.velocity.set(0,0,0);
                body.body.angularVelocity.set(Math.random()*5,Math.random()*5,Math.random()*5);
                body.body.position.set(Math.random()*20-10,50,15);
            }
        }
    }
    
    for(var i=0;i<players.length;i++){
        var player = players[i];
        if(player){
            if(player.body.position.y < -10){
                player.body.velocity.set(0,0,0);
                player.body.position.set(Math.random()*20-10,20,0);
            }
        }
    }
    
   
}, 1000 / 10);  



function sendStates(){
    var states = [];
    var bodies = [];
    for(var i=0;i<players.length;i++){
        var player = players[i];
        if(player && player.hasSpawned){
            states.push([player.id,parseFloat(player.body.position.x.toFixed(5)),parseFloat(player.body.position.y.toFixed(5)),parseFloat(player.body.position.z.toFixed(5)), player.lastProcessedInput]);
        }
    }
    
    for(var i=0;i<boxBodies.length;i++){
        var body = boxBodies[i];
        if(body){
            bodies.push([body.id, body.cType, parseFloat(body.body.position.x.toFixed(5)),parseFloat(body.body.position.y.toFixed(5)),parseFloat(body.body.position.z.toFixed(5)), parseFloat(body.body.quaternion.x.toFixed(5)),parseFloat(body.body.quaternion.y.toFixed(5)),parseFloat(body.body.quaternion.z.toFixed(5)),parseFloat(body.body.quaternion.w.toFixed(5))]);
        }
    }
    
    broadcastToAllSockets({m:6, p:states, b:bodies});
    
}



function broadcastToAllSockets(packet){
    for(var i=0,length=players.length;i<length;i++){
        var player = players[i];
        if(player){
            emitIfOpen(player.socket, packet);
        }
    }
}




world = createWorld();


 

Link to post
Share on other sites

@AJTJ My insight is that this is a pretty hard theme, just dont give up :) Multiplayer can be the most challenging thing but it's really cool to see when something is starting to work!

@Raggar This is a really cool example, seeing client side prediction (with stored inputs).

I'm making a physics based multiplayer game too. It's a bit early so nothing to show yet.

It is based on Colyseus, but a bit modified version of it - I have many objects in the world and one of the challenges is the spatial hashing of objects so i can find and only send the closest ones to the player.
The physics engine is the WASM version of Ammo (Bullet port). This runs really fast, nearly native speed, can handle a few thousand (!!!) moving capsules and about 1000 static objects on a big world. at 60 ticks/second.

With single player, I use a Worker with the same game logic that goes into multiplayer. The Worker uses WASM/Ammo too (in the browser). This is much slower (4x-5x), but can handle and synchronize about 500 capsules at 60FPS.

The bottleneck is with javascript workers the data transfer speed, even if positions are spatially filtered and sent only at 20/sec. (like in multiplayer)

It's a shame SharedArrayBuffer is disabled because of Spectre and Meltdown!!!!!

Also, the worker version is much slower in Edge (I think the data transfer is slow, but the physics engine is slower too), Chrome and FF is faster.

 

Link to post
Share on other sites

 

@Pryme8  @Wingnut @Raggar

So, I've been fiddling around with the examples that you've shown me. The problem with the physicsJoint solution is that my game relies on a "player vs gravity" mechanic and having that extra invisible mesh, while cool, presented more challenges than it solved.

All of the "move mesh x distance" solutions really didn't serve my game mechanic.

What seems to have worked the best is over-riding the default OS keypress delay, since it f**ks with my core mechanic. With my own key repetition delay (based on time) I can finesse the controls.

You can see the latest at http://aaronjanke.com/ballGame/ As you can tell, the controls are MUCH smoother and easier to play, and I've also increased the gravity.

I don't take responsibility for this solution, I found it here: https://stackoverflow.com/questions/3691461/remove-key-press-delay-in-javascript/3691661#3691661

Mine looks like this (drawn heavily from the above):

function KeyboardController(keys, repeat) {
 
 
var timers = {};
 
// When key is pressed and we don't already think it's pressed, call the
// key action callback and set a timer to generate another one after a delay
//
document.onkeydown = function (event) {
// console.log(event);
// console.log(keys);
var key = (event || window.event).keyCode;
if (!(key in keys))
return true;
if (!(key in timers)) {
timers[key] = null;
keys[key]();
if (repeat !== 0)
timers[key] = setInterval(keys[key], repeat);
}
return false;
};
 
// Cancel timeout and mark key as released on keyup
//
document.onkeyup = function (event) {
var key = (event || window.event).keyCode;
if (key in timers) {
if (timers[key] !== null)
clearInterval(timers[key]);
delete timers[key];
}
};
 
// When window is unfocused we may not get key events. To prevent this
// causing a key to 'get stuck down', cancel all held keys
//
window.onblur = function () {
for (key in timers)
if (timers[key] !== null)
clearInterval(timers[key]);
timers = {};
};
};
then:
 
KeyboardController({
 
// a key
65: () => playerMesh.applyImpulse(new BABYLON.Vector3(5, 0, 0), playerMesh.getAbsolutePosition()),
 
// w key
87: () => playerMesh.applyImpulse(new BABYLON.Vector3(0, 0, -5), playerMesh.getAbsolutePosition()),
 
// d key
68: () => playerMesh.applyImpulse(new BABYLON.Vector3(-5, 0, 0), playerMesh.getAbsolutePosition()),
 
// s key
83: () => playerMesh.applyImpulse(new BABYLON.Vector3(0, 0, 5), playerMesh.getAbsolutePosition())
 
// this is the delay between repeats
}, 50);
Link to post
Share on other sites

@BitOfGold

That's cool. CannonJS starts acting weird after a few hundred dynamic objects, unfortunately. I was planning on making a game with thousands of dynamic bodies at some point. And I do mean thousands at the same time, not spread out too much. Rendering will have to be faked with instances and sprites, but I don't think the web is ready for physics like that :P

On the client, I'm running my physics simulation in a worker as well to squeeze out some extra juice. I have a hard time wrapping my head around typed arrays, so for the time being, I'm just using posting JSON objects instead of transferables. All my static objects are merged into one compound body, as this gives some extra performance in CannonJS. For the time being, it's too late to switch physics engine. Maybe in the future. We'll see how this tech evolves.

I went back to websockets, as for some reason they seem to perform better than WebRTC using simple-peer on the client and either electron-webrtc or node-webrtc on the server. But the way I set up my comms, I can easily switch after I've done some testing and benchmarking.

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...