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.