Help with network multiplayer correction and prediction

I am attempting to create a multiplayer tank game. But before I get there I am simply creating a singular tank controlled and displayed on one device, while on the other connected device only displays the tank and its movements. My problem is that within a few moments they get out of sync.

I am using noobhub (https://github.com/Overtorment/NoobHub) to implement networking. I am using the server/node.js right from the git project. I allow the tank-controlling device to be the authority and have thus called that device the “host” but I run the noobhub server on my desktop. I send, at regular intervals, the slider values to the client device and allow it to calculate the x,y force to apply to each wheel.

This is my first time creating a networked game so I have been looking for examples and tutorials. At first I attempted to simply set a new x,y and rotation, when that failed miserably I kept looking and found this article (https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/) and began to try to implement its recommendation which is

The solution is to keep a circular buffer of past character state and input for the local player on the client, then when the client receives a correction from the server, it first discards any buffered state older than the corrected state from the server, and replays the state starting from the corrected state back to the present “predicted” time on the client using player inputs stored in the circular buffer. In effect the client invisibly “rewinds and replays” the last n frames of local player character movement while holding the rest of the world fixed.

I believe that I have created a circular buffer but stopped in my tracks when I came to realize two problems I have with implementing this solution.

First, I use system.getTimer() for determining the age of a stored state. From what I understand system.getTimer() only gets the time the application has been running for and will be different between devices.

My second problem is that I do not know how to “replay” the physics simulation. Because I have used the physics API I am completely aloof to how physics simulations are performed. How can I carry out the recalculation described in the article above?

Here is where I send data to the noobhub server to relay to the client

[lua]–From hostGame.lua

 – Update the tank
        local function transmitData(event)
            hub = params.gameHub;
            local dataString; – Create data string to send state to client
            – Tank body indexes; 1- 7
            – Tank left wheel indexes 8-12
            – Tank right wheel indexes 13-17
            – Time sent index 18
            local bodyIndex = 1; – Index of the body inthe tank group.
            local lWheelIndex = 2;
            local rWheelIndex = 3;
            local x,y; – An intermediate step

            x,y = tank.tankGroup[bodyIndex]:getLinearVelocity();
            dataString = tank.tankGroup[bodyIndex].x … “,” … tank.tankGroup[bodyIndex].y … “,” … tank.tankGroup[bodyIndex].rotation … “,” … tank.rightVel … “,” … tank.leftVel … “,”… x … “,” … y … “,”;
            x,y = tank.tankGroup[lWheelIndex]:getLinearVelocity();
            dataString = dataString … tank.tankGroup[lWheelIndex].x … “,” … tank.tankGroup[lWheelIndex].y … “,” … tank.tankGroup[lWheelIndex].rotation … “,” … x … “,” … y … “,”;
            x,y = tank.tankGroup[rWheelIndex]:getLinearVelocity();
            dataString = dataString … tank.tankGroup[rWheelIndex].x … “,” … tank.tankGroup[rWheelIndex].y … “,” … tank.tankGroup[rWheelIndex].rotation … “,” … x … “,” … y … “,”;
            dataString = dataString … system.getTimer();
            hub:publish({
                            message = { action = “update”, data = dataString, timestamp = system.getTimer()}
                        })
        end
        – Send data to hub
        --timer.performWithDelay(16, transmitData, 0);
                – This will be called each time data is transmitted, every 30MS
        local function moveTheTank(event)
            tank.rightVel = sliderRight.value;
            tank.leftVel = sliderLeft.value;
            transmitData();
            tank:move();
        end
        timer.performWithDelay(30, moveTheTank, 0)[/lua]

And this is where I receive the data on the other end

[lua]

–From joinGame.lua

– Move the tank every 30MS
        local function moveTheTank(event)
            – Move the tank
            tank:move();

            – Store state
                – Create data string to hold the state data
            local dataString; – used to store the state
            – Tank body indexes; 1- 7
            – Tank left wheel indexes 8-12
            – Tank right wheel indexes 13-17
            – Time sent index 18

            local bodyIndex = 1; – Index of the body inthe tank group.
            local lWheelIndex = 2;
            local rWheelIndex = 3;
            local x,y; – An intermediate step
            x,y = tank.tankGroup[bodyIndex]:getLinearVelocity();
            dataString = tank.tankGroup[bodyIndex].x … “,” … tank.tankGroup[bodyIndex].y … “,” … tank.tankGroup[bodyIndex].rotation … “,” … tank.rightVel … “,” … tank.leftVel … “,”… x … “,” … y … “,”;
            x,y = tank.tankGroup[lWheelIndex]:getLinearVelocity();
            dataString = dataString … tank.tankGroup[lWheelIndex].x … “,” … tank.tankGroup[lWheelIndex].y … “,” … tank.tankGroup[lWheelIndex].rotation … “,” … x … “,” … y … “,”;
            x,y = tank.tankGroup[rWheelIndex]:getLinearVelocity();
            dataString = dataString … tank.tankGroup[rWheelIndex].x … “,” … tank.tankGroup[rWheelIndex].y … “,” … tank.tankGroup[rWheelIndex].rotation … “,” … x … “,” … y … “,”;
            dataString = dataString … system.getTimer();

            --store in the buffer
            local data = {};
            data = ParseCSVLine(dataString, ‘,’)
            for i= 1, 18, 1 do
                data[i] = tonumber(data[i]);
            end
            movementBuffer[mBufferIndex] = data;
            mBufferIndex = (mBufferIndex + 1) % bufferSize;
        end

        – move the tank
        timer.performWithDelay(30, moveTheTank, 0); – 1000/30 = 33.3, I would like to decrease this to 1000/60 = 16.6
        local data = {};

        – Fetch data from the hub.
        local function getData(event)
            hub = params.gameHub;
            local dataString;

            hub:subscribe({
                                channel = params.hubInfo[3], – get the channel
                                callback = function(message)
                                    if(message.action == “update”) then
                                        dataString = message.data;
                                        --print(dataString)
                                        – Parse data string
                                        data = ParseCSVLine(dataString, ‘,’);
                                        for i= 1, 18, 1 do
                                            data[i] = tonumber(data[i]);
                                        end
                                        --Search for where to insert in the movement buffer.
                                        for i = 0, bufferSize, 1 do
                                            if(movementBuffer[i] ~= nil and movementBuffer[i][18] ~= nil) then
                                                if(movementBuffer[i][18] > data[18]) then
                                                    for j = 0, bufferSize, 1 do
                                                        if(movementBuffer[j] ~= nil and movementBuffer[j][18] ~= nil) then
                                                            if(movementBuffer[j][18] < data[18]) then
                                                                movementBuffer[j - 1] = data;
                                                            end
                                                        end
                                                    end
                                                end
                                            end
                                        end
                                        – Remove all data that is older than the new data
                                        for i = 0, bufferSize, 1 do
                                            if(movementBuffer[i] ~= nil and movementBuffer[i][18] ~= nil) then
                                                if(movementBuffer[i][18] < data[18]) then
                                                    movementBuffer[i] = nil;
                                                end
                                            end
                                        end
                                     – Set the input values for the next tank:move call.
                                        tank.rightVel = tonumber(data[4]);
                                        tank.leftVel  = tonumber(data[5]);
                                    end

                                end;
                                    });
        end
        getData();

[/lua]

Finally this is how I calculate the X,Y force to apply to each wheel.

[lua]

– From tank.lua

function Tank:move()
    – Do movement stuff.
    local speedMod = 32;
    local rWheel = 2; – Index of rWheel
    local lWheel = 3; – Index of lWheel
    if(self.rightVel > 56 or self.rightVel < 46) then – Create a 30 pixel wide dead-zone
        self.tankGroup[rWheel].speedX = math.floor((self.rightVel - 50) * math.cos(math.rad(self.tankGroup[rWheel].rotation + 90)));
        self.tankGroup[rWheel].speedY = math.floor((self.rightVel - 50) * math.sin(math.rad(self.tankGroup[rWheel].rotation + 90)));
        self.tankGroup[rWheel]:setLinearVelocity(self.tankGroup[rWheel].speedX * speedMod, self.tankGroup[rWheel].speedY * speedMod)
    end
    if(self.leftVel > 56 or self.leftVel < 46) then
        self.tankGroup[lWheel].speedX = math.floor((self.leftVel - 50) * math.cos(math.rad(self.tankGroup[lWheel].rotation + 90)));
        self.tankGroup[lWheel].speedY = math.floor((self.leftVel - 50) * math.sin(math.rad(self.tankGroup[lWheel].rotation + 90)));
        self.tankGroup[lWheel]:setLinearVelocity(self.tankGroup[lWheel].speedX * speedMod, self.tankGroup[lWheel].speedY * speedMod)
    end
end

[/lua]

Finally, I’ve had a very hard time finding more information to help understand how to implement real time multiplayer in this context. Any sources to look up would be appreciated.

I have attached the project files so you can take a closer look at it. I did not include the node.js but you can get it from the noobhub repository mentioned at the beginning of the post.

I’m afraid I couldn’t read all of the post above, but I do think I can give you some insights into why you may be seeing issues.

Meanwhile, you may want to wait for someone else to reply who has used the same API.

I should also ask, does the API come with demos and have you run them?

If it does and they behave well, you might want to closely examine what they are doing versus what you are. You could have missed something.

MP Is Hard

If you want to successfully maintain positions, orientations, velocities, etc.  Then you need to:

  • Send position updates for all controlled objects from the authority to the clients on a regular basis. 
  • You must override client values with those sent by the authority.
  • You may also need to implement flight-time updates.   
  • Just sending control values like, turn left, move forward, rotate 45 degrees, stop. WILL NOT WORK.

Flight Time

In any MP scenario, there will always be some amount of time between when the authority records a value (say position X) of a client object and when the value is finally received by the client and used.  This is flight time.  

If the flight time is significant, you will see odd behavior and lag on the clients.  Your game may fall out of sync as a result.

If you’re not using a library designed to deal with this, you may have to handle this problem on your own.  However, doing so is NOT easy.  This is why coding MP libraries is a full-time job for multiple programmers on serious game projects.

In a nutshell, you will need to find a way to estimate the running-average for flight-time from the authority to each client individually (each will be different). 

The authority can measure this time via round-trip communication measurements, then send half-that measured value to the specific client as part of the update package. 

(You’ll need to keep measuring this and updating it, and this will add overhead to your communications.  Check to see if the API you are using already has this information, as it may.)

Once a client is receiving update packages that include an estimated flight-time value, the client can modify the way it applies authority values based on that time.  

Ex: If the authority sends an update to client-A with these values:

  • x = 0
  • y = 0
  • velocity x = 100 (pixels per second)
  • velocity y = 0
  • estimated flight time = 25 ms

If you examine this data carefully, you can easily see it would be a mistake to simply apply the values < 0, 0 > as your < x,y,> respectively.  The proper position is probably closer to: < 25 * 100 / 1000 , 0 > or < 3, 0 >

Of course, this is a simple example and when you start to involve other bodies, near collisions, tunneling detection, damping, … it gets super painful to accurately model the correct behavior of the client objects.

With regards to your circular buffer question and time.  Yes, system.getTimer() is specific to each device.  You need to maintain a common time, controlled by the authority.

The simplest way to do this is:

  • Authority sends out ‘current time’ indicator to all clients.
  • All clients compare ‘current time’ value to their local time and keep a delta value.
  • Subsequently, the authority can send out time-tagged data to clients and each client can determine their ‘true-time’ using the delta they calculated before.
  • Now, the clients can use their adjusted times to make decisions about what data to consume.

Note: Deltas will fall out of sync, so you need to update the ‘current time’ on a regular basis. i.e. The authority needs to force clients to re-calculate the delta time on a regular basis.

Note 2: This is a simpler alternative to calculating round-trip flight times and sending estimated flight times, but may be a little less accurate.

I’m afraid I couldn’t read all of the post above, but I do think I can give you some insights into why you may be seeing issues.

Meanwhile, you may want to wait for someone else to reply who has used the same API.

I should also ask, does the API come with demos and have you run them?

If it does and they behave well, you might want to closely examine what they are doing versus what you are. You could have missed something.

MP Is Hard

If you want to successfully maintain positions, orientations, velocities, etc.  Then you need to:

  • Send position updates for all controlled objects from the authority to the clients on a regular basis. 
  • You must override client values with those sent by the authority.
  • You may also need to implement flight-time updates.   
  • Just sending control values like, turn left, move forward, rotate 45 degrees, stop. WILL NOT WORK.

Flight Time

In any MP scenario, there will always be some amount of time between when the authority records a value (say position X) of a client object and when the value is finally received by the client and used.  This is flight time.  

If the flight time is significant, you will see odd behavior and lag on the clients.  Your game may fall out of sync as a result.

If you’re not using a library designed to deal with this, you may have to handle this problem on your own.  However, doing so is NOT easy.  This is why coding MP libraries is a full-time job for multiple programmers on serious game projects.

In a nutshell, you will need to find a way to estimate the running-average for flight-time from the authority to each client individually (each will be different). 

The authority can measure this time via round-trip communication measurements, then send half-that measured value to the specific client as part of the update package. 

(You’ll need to keep measuring this and updating it, and this will add overhead to your communications.  Check to see if the API you are using already has this information, as it may.)

Once a client is receiving update packages that include an estimated flight-time value, the client can modify the way it applies authority values based on that time.  

Ex: If the authority sends an update to client-A with these values:

  • x = 0
  • y = 0
  • velocity x = 100 (pixels per second)
  • velocity y = 0
  • estimated flight time = 25 ms

If you examine this data carefully, you can easily see it would be a mistake to simply apply the values < 0, 0 > as your < x,y,> respectively.  The proper position is probably closer to: < 25 * 100 / 1000 , 0 > or < 3, 0 >

Of course, this is a simple example and when you start to involve other bodies, near collisions, tunneling detection, damping, … it gets super painful to accurately model the correct behavior of the client objects.

With regards to your circular buffer question and time.  Yes, system.getTimer() is specific to each device.  You need to maintain a common time, controlled by the authority.

The simplest way to do this is:

  • Authority sends out ‘current time’ indicator to all clients.
  • All clients compare ‘current time’ value to their local time and keep a delta value.
  • Subsequently, the authority can send out time-tagged data to clients and each client can determine their ‘true-time’ using the delta they calculated before.
  • Now, the clients can use their adjusted times to make decisions about what data to consume.

Note: Deltas will fall out of sync, so you need to update the ‘current time’ on a regular basis. i.e. The authority needs to force clients to re-calculate the delta time on a regular basis.

Note 2: This is a simpler alternative to calculating round-trip flight times and sending estimated flight times, but may be a little less accurate.