🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Server Side Simulation Synchronization with Client Box2d

Started by
2 comments, last by hplus0603 4 years, 8 months ago

I am running a simple box2d simulation on nodejs server.(planck.js) 2 bodies are connected to each other with distance joint and one is orbiting around another.

I also have a java libgdx client with box2d.

There are box2d world and bodies created both on client and server but for now i am not steping client side world because i am not sure how to sync client world and server world.What is happening right now is server is running the simulation,sending position data to client and client sets the position without ticking box2d world(which i know is wrong,i should run simulation at both sides and sync somehow because of latency)

I tried to fix timesteps of client and server because i guess its the first step of  server  authoritative networking but i am not sure whether i did it correct or not ?This is code from server side:


 var lastUpdate = 0
    var accumulator = 0
var TIMESTEP = 1/60


        function startSimulation(){
            lastUpdate = Date.now();
                setInterval(tick,1000/60);
                setInterval(sendMovingData,40)

        }
        function tick(){

            var now = Date.now();
            var dt = now - lastUpdate;
            lastUpdate = now;

            gameLoop(dt);
            }


        function gameLoop(dt){
            var frameTime = Math.min(dt, 0.25)
                        accumulator += frameTime

                        while (accumulator>= TIMESTEP) {

                            world.step(TIMESTEP, 6, 2);
                            accumulator -= TIMESTEP;
                            setConstantVelocity()// applys forces on movingBody

                        }

        }
        function sendMovingData(){
        //sends position data to client
        }

If i use this code and just set position on client obviously result is pretty bad: (client is just seting positions world is not ticking) please checkout this video

https://streamable.com/v8r6h

If i run the simulation just on client locally without server this is how it looks: https://streamable.com/8r63k

Client side:


fun render() {
                var frameTime = Math.min(Gdx.graphics.deltaTime, 0.25f)
                accumulator += frameTime

                while (accumulator>= TIMESTEP) {


                        world.step(TIMESTEP, 6, 2);// comment this if you recieve data from server


                    accumulator -= TIMESTEP;
                   setConstantVelocity() // comment this if you recieve data from server

                }

So i read a ton about this on internet tried some interpolation stuff and client side prediction but couldnt make it work.I guess i need some code example.By the way i am not sending any input from client to server right now.

Is this correct implementation of fixed time step ?

How do i run simulation on both sides and have a smooth movement of circle while keeping server authoritative? (i guess this is releated to client side prediction but i am kinda confused and looking for examples )

Advertisement

I couldnt edit my question so i am posting here:

I also tried to run simulation on both sides and implement something similar to client side prediction.Check this code and result please.
Server:

 


   var lastUpdate = 0
            var accumulator = 0
        var TIMESTEP = 1/60
            
            
                function startSimulation(){
                    lastUpdate = Date.now();
                        setInterval(tick,1000/60);
                
                }
                function tick(){
                
                    var now = Date.now();
                    var dt = now - lastUpdate;
                    lastUpdate = now;
                
                    gameLoop(dt);
                    }
                
                
                function gameLoop(dt){
                    var frameTime = Math.min(dt, 0.25)
                                accumulator += frameTime
                
                                while (accumulator>= TIMESTEP) {
                
                                       world.step(TIMESTEP, 6, 2);
                        accumulator -= TIMESTEP;
                        setConstantVelocity()
                        tickNumber++
                        sendMovingData()
                                    
                
                                }
                
                
                
                }
                function sendMovingData(){
                //sends position data and tick number to client
                }

Now client side is a little bit more different:

         


fun render() {
                        var frameTime = Math.min(Gdx.graphics.deltaTime, 0.25f)
                        accumulator += frameTime
        
                        while (accumulator>= TIMESTEP) {
        
        
                                    world.step(TIMESTEP, 6, 2);
        
                                    setConstantVelocity()
                                    tickNumber++
        
        
        
                            accumulator -= TIMESTEP;
        
        
                        }
        }
    
     
    
           fun setConstantVelocity(){
            //apply forces to movingBody
               var data:JSONObject = JSONObject()
                    data.put("x",movingBody.position.x)
                    data.put("y",movingBody.position.y)
                    data.put("tickNumber",tickNumber)
                    pendingInputs.add(data) // there is an arraylist for saving client moves.
            
            }
    
    fun recieveIncomingData(){ // called when server data arrives
    var serverTickNumber = data.get("tickNumber").toString().toInt()
    
            var newPositionVector = Vector2(data.get("x").toString().toFloat(),data.get("y").toString().toFloat())
            movingBody.setTransform(newPositionVector,0f)
    
           var j = 0
            while (j < pendingInputs.size)
            {
                var clientMove = pendingInputs.get(j)
                if (clientMove.get("tickNumber").toString().toInt() <= serverTickNumber) // recvd from server
                {
                    pendingInputs.removeAt(j)
                }
                else
                {
                    var clientMoveVector:Vector2 = Vector2(clientMove.get("x").toString().toFloat(),clientMove.get("y").toString().toFloat())
                    movingBody.setTransform(clientMoveVector,0f)
    
                    j++;
                }
            }
    
    
    
    }

This is the result of  running both on client and server:
https://streamable.com/vwxxb

I feel like server side simulation is running much faster than client(?)

One of the problems seems to be that the update stream to the client is jerky. This is, unfortunatley, a necessary outcome of using websockets for communication, because of TCP. Many web games like agar.io or slither.io have the same problem.

Another of the problems seems to be that your clocks aren't quite locked to real time. On the server, you'll want to do something like:


const TICK_RATE = 60;

let startTime = Date.now();
let currentTick = 0;

function simulateUntilNow(simFunction) {
  let targetTick = Math.floor((Date.now() - startTime) * TICK_RATE);
  while (currentTick < targetTick) {
    currentTick++;
    simFunction(currentTick);
  }
}

...

setInterval(function () { simulateUntilNow(mySimulation); }, 1000/TICK_RATE/2);

You would use this by implementing your world simulation tick function as the mySimulation callback in the above code, and the runtime will attempt to keep the simulation up to date with current time, one fixed-size step at a time. When the clock drifts, you may end up with zero or two steps per callback -- to minimize the effect of this, I doubled the rate of callback from the actual tick rate, which means you will have a number of callbacks that simulate nothing, but when you fall behind, you will skew by half a tick rather than a full tick, and then catch up.

For the client, the problem is that it needs to synchronize "currentTick" as well as "startTime" with the server. Easiest is to include "currentTick" in each network update packet, and then set "startTime" locally to match up with the server time:

function syncToReceivedServerTime(serverTick) {
  startTime = Date.now() - serverTick * TICK_RATE;
}

this will re-establish the server/client time relationship such that the client will attempt to catch up -- or will pause, if it has been running ahead of the server.

These adjustments are "all at once" -- for smoother movement, you can either play tricks with the rate that time proceeds, or you can play tricks with the velocity of entities on the screen, or both. However, both those systems add a whole second level of complexity, so until you have this above system up and running, I would suggest not worrying about hiding that yet.

(Note that code snippets are untested -- hopefully they will show the way)

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement