Purple Martians
Technical Code Descriptions
Netgame - Client Timing Sync
Client Timing Sync
Server sends 'stdf' packet
Client calculates dsync
Determine the setpoint
Adjust client timer
Entire client_timer_adjust() function
Client Timing Sync
Timing is critical in netgame. The server and client must have a tightly controlled timing relationship.
The server is the master timing source, running at 40 frames per second.
Clients are responsible for measuring and maintaining their timing offset in relation to the server.
Clients use 'stdf' packets sent from the server as their timing reference.
The measure of the time difference is called dsync.
dsync = stdf_dst frame - current frame
If the stdf from the server matches the current frame, then dsync is zero.
If the stdf is from the past, then dysnc is positive.
This means that when it is applied, the client will go back to a previous state.
After doing that, the client then fast forwards to the current frame.
If the stdf is from the future, then dsync is negative.
This means that when applied, the client jump forward in time.
The is the least common scenario, and should rarely happen.
Server sends 'stdf' packet
The server sends 'stdf' packets to clients every frame.
These stdf packets contain pieces of a compressed dif state, and a header that describes the contents.
From that header, the destination frame number is the current frame number on the server when the dif was created and sent.
That is what the client uses to measure its timing relation to the server.
mPacketBuffer.PacketName(data, pos, "stdf");
mPacketBuffer.PacketPutInt4(data, pos, src);
mPacketBuffer.PacketPutInt4(data, pos, dst);
....
ServerSendTo(data, pos, mPlayer.loc[p].who);
Client calculates dsync
Every frame the client processes all 'stdf' packets in the packet buffer.
The difference between the destination frame and the current frame makes a crude integer dsync only accurate to one frame (or 25ms)
This is combined with the timestamp recorded by packet buffer when the packet was actually received.
Then the maximum dysnc is calculated and returned.
float mwPacketBuffer::get_max_dsync(void)
{
float max_dsync = -1000;
lock_mutex();
// iterate all stdf packets, calc dysnc and max dysnc
for (int i=0; i < 200; i++)
if ((rx_buf[i].active) && (rx_buf[i].type == 11))
{
int dst = 0;
memcpy(&dst, rx_buf[i].data+8, 4);
// calc dysnc
float csync = (float)(dst - mLoop.frame_num) * 0.025; // crude integer sync based on frame numbers
float dsync = al_get_time() - mPacketBuffer.rx_buf[i].timestamp + csync; // add time between now and when the packet was received into packet buffer
if (dsync > max_dsync) max_dsync = dsync;
}
unlock_mutex();
return max_dsync;
}
Then dsync is averaged in 'client_timer_adjust()'.
void mwNetgame::client_timer_adjust(void)
{
float max_dsync = mPacketBuffer.get_max_dsync(); // iterate all stdf packets, calc dysnc, then get max dysnc
int p = mPlayer.active_local_player;
if (max_dsync > -1000)
{
mPlayer.loc[p].dsync = max_dsync;
mRollingAverage[2].add_data(mPlayer.loc[p].dsync); // send to rolling average
mPlayer.loc[p].dsync_avg = mRollingAverage[2].avg; // get average
.....
Determine the setpoint
Before we adjust the timer, we need two things:
- a measured value, 'dsync_avg', which we just calculated
- a setpoint (what we want the value to be)
The setpoint is called 'client_chase_offset', and it is set from one of three things:
- manually set by the client to a static value
- automatically set by the client based on ping.
- manually set by the server (mostly for testing and debug purposes)
// automatically adjust client_chase_offset based on ping time
if (client_chase_offset_mode == 1) client_chase_offset = - mPlayer.loc[p].ping_avg + client_chase_offset_auto_offset;
// overridden by server
if (mPlayer.syn[0].server_force_client_offset) client_chase_offset = mPlayer.syn[0].client_chase_offset;
// set point
float sp = client_chase_offset;
Adjust client timer
At this point we have a measured value and a setpoint.
Next the client timer is adjusted to make the measured value approach the setpoint.
I was experimenting with implementing a PID control loop here, but found it was not needed, so I just use the proportional term.
// error = set point - measured value
float err = sp - mPlayer.loc[p].dsync_avg;
// instantaneous error adjustment (proportional)
float p_adj = err * 80;
// cumulative error adjust (integral)
tmaj_i += err;
float i_clamp = 5;
if (tmaj_i > i_clamp) tmaj_i = i_clamp;
if (tmaj_i < -i_clamp) tmaj_i = -i_clamp;
tmaj_i *= 0.95; // decay
float i_adj = tmaj_i * 0;
// combine to get total adjust
float t_adj = p_adj + i_adj;
// adjust speed
float fps_chase = mLoop.frame_speed - t_adj;
if (fps_chase < 10) fps_chase = 10; // never let this go negative
if (fps_chase > 70) fps_chase = 70;
al_set_timer_speed(mEventQueue.fps_timer, (1 / fps_chase));}
Entire client_timer_adjust() function
void mwNetgame::client_timer_adjust(void)
{
float max_dsync = mPacketBuffer.get_max_dsync(); // iterate all stdf packets, calc dysnc, then get max dysnc
int p = mPlayer.active_local_player;
if (max_dsync > -1000)
{
mPlayer.loc[p].dsync = max_dsync;
mRollingAverage[2].add_data(mPlayer.loc[p].dsync); // send to rolling average
mPlayer.loc[p].dsync_avg = mRollingAverage[2].avg; // get average
// automatically adjust client_chase_offset based on ping time
if (client_chase_offset_mode == 1) client_chase_offset = - mPlayer.loc[p].ping_avg + client_chase_offset_auto_offset;
// overidden by server
if (mPlayer.syn[0].server_force_client_offset) client_chase_offset = mPlayer.syn[0].client_chase_offset;
// set point
float sp = client_chase_offset;
// error = set point - measured value
float err = sp - mPlayer.loc[p].dsync_avg;
// instantaneous error adjustment (proportional)
float p_adj = err * 80;
// cumulative error adjust (integral)
tmaj_i += err;
float i_clamp = 5;
if (tmaj_i > i_clamp) tmaj_i = i_clamp;
if (tmaj_i < -i_clamp) tmaj_i = -i_clamp;
tmaj_i *= 0.95; // decay
float i_adj = tmaj_i * 0;
// combine to get total adjust
float t_adj = p_adj + i_adj;
// adjust speed
float fps_chase = mLoop.frame_speed - t_adj;
if (fps_chase < 10) fps_chase = 10; // never let this go negative
if (fps_chase > 70) fps_chase = 70;
al_set_timer_speed(mEventQueue.fps_timer, (1 / fps_chase));
mPlayer.loc[p].client_chase_fps = fps_chase;
mLog.add_tmrf(LOG_TMR_client_timer_adj, 0, "dsc:[%5.2f] dsa:[%5.2f] sp:[%5.2f] er:[%6.2f] pa:[%6.2f] ia:[%6.2f] ta:[%6.2f]\n", mPlayer.loc[p].dsync*1000, mPlayer.loc[p].dsync_avg*1000, sp*1000, err*1000, p_adj, i_adj, t_adj);
mLog.addf(LOG_NET_timer_adjust, p, "timer adjust dsc[%5.1f] dsa[%5.1f] off[%3.1f] chs[%3.3f]\n", mPlayer.loc[p].dsync*1000, mPlayer.loc[p].dsync_avg*1000, sp*1000, fps_chase);
}
// else mLog.addf(LOG_NET_timer_adjust, p, "timer adjust no stdf packets this frame dsc[%5.1f] dsa[%5.1f]\n", mPlayer.loc[p].dsync*1000, mPlayer.loc[p].dsync_avg*1000);
}