Authoritative server movement in a realtime game

Options
ximael
edited September 2013 in Photon Server
How it should be properly made?

I am making a game with a simple tile 2d physics. Authoritative server and client with prediction. Server wrote from scratch but it is like Lite.
I can not synchronize ticks, because delta time floats +/- on both server and client. Scheduler/coroutine waitforseconds precision is bad, delta is about 63 ms on the server and about 40-80 ms on the client instead of 50. I tried to use time correction, but it was even worse, because of a bad precision.
To correct a client position I rewind and replay from last confirmed state. Almost every update I correct position(error > 0.05 meters). But player moving is jerky. I mean, like this(going to the right):
0>
---0>
-0>
----0>
--0>
0>
Server

Equivalent of Room class from Lite:
[code2=csharp]public void StartGame()
{
Dictionary<byte, object> data = new Dictionary<byte, object> { { Prm.GAME_START_TIME, Environment.TickCount + 2000 } };
PublishEvent(EvCode.GAME_START, data, false);
ScheduleAction(updateAction, 2150);//game starts in 2000ms + update interval 50 ms + server delay 100ms(server must receive all client messages from the future)
gameStarted = true;
gameTime=0;
}

public void Update()
{
int nowms = Environment.TickCount;
if (nowms > lastUpdateTime) deltaMs = nowms - lastUpdateTime;
else deltaMs = nowms - Int32.MinValue + Int32.MaxValue - lastUpdateTime;
if (deltaMs > 100) deltaMs = 100;
lastUpdateTime = nowms;
gameTime += deltaMs;

foreach (GameObject go in goList.Values)
{
if (go.enabled) go.Update(deltaMs);//movement and collisions calculated here
}
BroadcastPositions();

if (gameStarted) ScheduleAction(updateAction, 50);
}

void BroadcastPositions(bool forced = false)
{
MemoryStream ms = new MemoryStream();
ms.Write(BitConverter.GetBytes(gameTime), 0, 4);//timestamp
foreach (GameObject go in goList.Values)
{
if (forced || go.isPChanged)
{
ms.Write(BitConverter.GetBytes(go.id), 0, 2);//object id
ms.Write(BitConverter.GetBytes((float)go.x), 0, 4);//object X
ms.Write(BitConverter.GetBytes((float)go.y), 0, 4);//object Y
}
}
Dictionary<byte, object> data = new Dictionary<byte, object> { { Prm.GO_POSITIONS, ms.ToArray() } };
PublishEvent(EvCode.GO_POSITIONS, data, true);
}[/code2]

Peer class:
[code2=csharp]protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
switch (operationRequest.OperationCode)
{
case OpCode.PLAYER_CONTROLS:
{
if (player != null operationRequest.Parameters.ContainsKey(Prm.PLAYER_CONTROLS))
{
byte[] kss = (byte[]) operationRequest.Parameters[Prm.PLAYER_CONTROLS];
uint clientGameTime = BitConverter.ToUInt32(kss, 0);//client game time
byte keyMask = kss[4];//Keyboard input
player.SetKeyMask(clientGameTime, keyMask);
}
}
break;
}
}[/code2]

Player class
[code2=csharp]struct Tskey
{
public uint timestamp;
public byte keymask;
}
Tskey[] tskey = new Tskey[5];

public void SetKeyMask(uint evTime, byte km)
{
for (int i = tskey.Length - 1; i > 0; i--)
{
tskey = tskey[i - 1];
}
tskey[0].timestamp = evTime;
tskey[0].keymask = km;
}

public override void Update(byte deltaMs)
{
for (byte i = 0; i < tskey.Length; i++)
{
if (tskey.timestamp < game.gameTime)
{
keyMask = tskey.keymask;
break;
}
}
Transform(deltaMs);
}[/code2]

Client

Game class
[code2=csharp]public void OnGameEvent(EventData eventData)
{
switch (eventData.Code)
{
case EvCode.GAME_START:
HandleEvGameStart(eventData.Parameters);
break;
case EvCode.GO_POSITIONS:
if (data.ContainsKey(Prm.GO_POSITIONS))
{
ApplyGOPositons((byte[])data[Prm.GO_POSITIONS]);
}
else Debug.Log("OnGameEvent ERROR PLAYERS_POSITIONS");
break;
}
}

void ApplyGOPositons(byte[] poss)
{
int i = 0;
byte off = 4;
serverGameTime = BitConverter.ToUInt32(poss, 0);
if (serverGameTime < lastServerGameTime) return;
lastServerGameTime = serverGameTime;

int pc = (poss.Length - off)/10;
short goid = 0;
if (pc != 0) goid=BitConverter.ToInt16(poss, off + i*10);
foreach (KeyValuePair<short, NetObject> kv in gameObjectList)
{
if (i < pc && kv.Key == goid)
{
float px = BitConverter.ToSingle(poss, off + i * 10 + 2);
float py = BitConverter.ToSingle(poss, off + i * 10 + 6);
gameObjectList[goid].SetPosState(serverGameTime, px, py);
i++;
if (i < pc) goid = BitConverter.ToInt16(poss, off + i * 10);
}
else
{
goList[kv.Key].SetPosState(serverGameTime);
}
}
}

void HandleEvGameStart(Dictionary<byte, object> data)
{
if (data.ContainsKey(Prm.GAME_START_TIME))
{
int gameStartTime = (int) data[Prm.GAME_START_TIME];
}
else Debug.LogError("GameController.HandleEvGameStart ERROR 1");

float startIn = (gameStartTime - photonHandler.peer.ServerTimeInMilliSeconds - photonHandler.peer.RoundTripTime / 2) / 1000f;

StartCoroutine(StartGame(startIn));
}

IEnumerator StartGame(float waitTime)
{
yield return new WaitForSeconds(waitTime);
gameStarted = true;
gameTime = 0;
StartCoroutine(UpdCoroutine());
}

IEnumerator UpdCoroutine()
{
lastUpdateTime = Environment.TickCount;
yield return new WaitForSeconds(50 / 1000f);
while (gameStarted)
{
int nowms = Environment.TickCount;
int deltaMs2;
if (nowms > lastUpdateTime) deltaMs2 = nowms - lastUpdateTime;
else deltaMs2 = nowms - Int32.MinValue + Int32.MaxValue - lastUpdateTime;
if (deltaMs2 > updIntMs * 2) deltaMs2 = updIntMs * 2;
deltaMs = (byte)deltaMs2;
lastUpdateTime = nowms;

float delta = deltaMs / 1000f;
gameTime += deltaMs;

if (lastKeyMask != keyMask)
{
MemoryStream ms = new MemoryStream();
ms.Write(BitConverter.GetBytes(gameTime), 0, 4);
ms.WriteByte(keyMask);
Dictionary<byte, object> data = new Dictionary<byte, object> { { Prm.PLAYER_CONTROLS, ms.ToArray() } };
photonHandler.SendGameRequest(OpCode.PLAYER_CONTROLS, data, true);
}
player.PredictionUpdate(delta);
lastKeyMask = keyMask;
keyMask = 0;
yield return new WaitForSeconds(50 / 1000f);
}
}[/code2]

Player class
[code2=csharp]public struct RState
{
public uint timestamp;
public Vector3 pos;
}
public struct SState
{
public uint timestamp;
public byte keymask;
public Vector3 pos;
}

public RState[] rstate = new RState[20];
public byte rstateCount;
public SState[] sstate = new SState[20];
public byte sstateCount;

public void SetPosState(uint gameTime, double x, double y)
{
for (int i = rstate.Length - 1; i > 0; i--)
{
rstate = rstate[i - 1];
}
rstate[0].timestamp = gameTime;
rstate[0].pos = new Vector3((float)x, 0, (float)y);
rstateCount = (byte) Mathf.Min(rstateCount + 1, rstate.Length);
if (netPredict) CheckSimStates();
}

public void UpdSim(float delta)
{
for (int i = sstate.Length - 1; i > 0; i--)
{
sstate = sstate[i - 1];
}
sstate[0].timestamp = game.gameTime;
sstate[0].keymask = game.keyMask;
UpdSState(0);
sstateCount = (byte) Mathf.Min(sstateCount + 1, sstate.Length);
}

private void CheckSimStates()
{
byte pc = 0;
byte nc = 0;
byte cc = 0;
for (byte i = 0; i < sstateCount; i++)
{
short q = (short)(sstate.timestamp - rstate[0].timestamp);
if (q < 0)
{
pc = i;
nc = (byte) (i - 1);
cc = (rstate[0].timestamp - sstate[pc].timestamp) <= (sstate[nc].timestamp - rstate[0].timestamp) ? pc : nc;
break;
}
}
float disterr = Vector3.Distance(rstate[0].pos, sstate[cc].pos);
float maxdisterr = 0.05f;
if (disterr > maxdisterr)
{
if (cc > 0)
{
sstate[cc].timestamp = rstate[0].timestamp;
sstate[cc].pos = rstate[0].pos;
for (byte i = (byte) (cc - 1); i >= 0 && i < sstateCount; i--)
{
UpdSState(i);
}
}
else
{
sstate[cc].pos = rstate[0].pos;
}
}
}[/code2]

Comments

  • As far as different deltas between client and server, I would highly recommend using a fixed timestep for your update. That is, your physics tick executes some fixed number of times per second (for instance, in Unity FixedUpdate always runs 50 times per second regardless of framerate). That way timestep never differs between client and server.
    Additionally, for best results, server shouldn't update the player unless it receives input from the client, otherwise if the server goes even one update without receiving input from the client (temporary network delay, for instance), it will desync.
  • I am trying to use update with a fixed delta. In unity I start Coroutine and WaitForSeconds(50 / 1000f), On the server I use ScheduleAction(updateAction, 50). But delta is still floating on the server and on the client.
    Is there a way to guarantee update delta time precision on the server and on the client, which may be run on different hardware and different OS?
    Server sends updates only after all clients already sent their input. I use start delay on the server, so all client input messages coming from the future for the server. Inputs stored in the array and pops when it's time.
  • The idea is that you DON'T need to measure delta. Of course, it's possible (and more than likely) that the ACTUAL time in between updates will vary - that's only natural. But, the idea is that because it updates some fixed number of times per second (say, it always runs 50 times per second - even if the framerate is only 30, some of those frames will get two or more updates in order to compensate), you can substitute a fixed value - for 50 frames a second, you simply use the value 1 / 50 = 0.02 as your timestep.