private static object m_last_messages_lock = new object(); // lock used to guard access to buffer contents AND m_last_update_time private static void EnqueueToRing(NewPlayerSnapshotToClientMessage msg, bool estimateVelocities = false) { // For old snapshots, we will fill in the ship velocity if that information is available. if (estimateVelocities) { var last_snapshots = m_last_messages_ring[m_last_messages_ring_pos_last]; foreach (var snapshot in msg.m_snapshots) { var last_snapshot = last_snapshots.m_snapshots.FirstOrDefault(m => m.m_net_id == snapshot.m_net_id); if (last_snapshot != null) { snapshot.m_vel = (snapshot.m_pos - last_snapshot.m_pos) / Time.fixedDeltaTime; snapshot.m_vrot = (Quaternion.Inverse(snapshot.m_rot) * Quaternion.SlerpUnclamped(last_snapshot.m_rot, snapshot.m_rot, 1f / Time.fixedDeltaTime)).eulerAngles; } } } m_last_messages_ring_pos_last = (m_last_messages_ring_pos_last + 1) & 3; m_last_messages_ring[m_last_messages_ring_pos_last] = msg; if (m_last_messages_ring_count < 4) { m_last_messages_ring_count++; } //Debug.LogFormat("Adding {0} at {1}, have {2}", msg.m_timestamp, Time.time, m_last_messages_ring_count); }
// when using the new protocol, this object replaces // Client.m_PendingPlayerSnapshotMessages //public static Queue<NewPlayerSnapshotToClientMessage> m_PendingPlayerSnapshotMessages = new Queue<NewPlayerSnapshotToClientMessage>(); public static void OnNewPlayerSnapshotToClient(NetworkMessage msg) { if (NetworkMatch.GetMatchState() == MatchState.PREGAME || NetworkMatch.InGameplay()) { NewPlayerSnapshotToClientMessage item = msg.ReadMessage <NewPlayerSnapshotToClientMessage>(); MPClientShipReckoning.AddNewPlayerSnapshot(item); } }
// extrapolate a whole NewPlayerSnapshotToClientMessage // extrapolate from B into t seconds into the future, t can be negative // this is used for generating synthetic snapshot messages in case we detected missing packets private static NewPlayerSnapshotToClientMessage ExtrapolatePlayerSnapshotMessage(NewPlayerSnapshotToClientMessage B, float t) { NewPlayerSnapshotToClientMessage C = new NewPlayerSnapshotToClientMessage(); int i; for (i = 0; i < B.m_num_snapshots; i++) { ExtrapolatePlayerSnapshot(ref C.m_snapshots[C.m_num_snapshots++], B.m_snapshots[i], t); } C.m_server_timestamp = B.m_server_timestamp + t; return(C); }
public static NewPlayerSnapshot GetPlayerSnapshot(NewPlayerSnapshotToClientMessage msg, Player p) { for (int i = 0; i < msg.m_num_snapshots; i++) { NewPlayerSnapshot playerSnapshot = msg.m_snapshots[i]; Player candidate = (Player)_Client_GetPlayerFromNetId_Method.Invoke(null, new object[] { playerSnapshot.m_net_id }); if (candidate == p) { return(playerSnapshot); } } return(null); }
public static bool Prefix(NetworkMessage msg) { if (NetworkMatch.GetMatchState() == MatchState.PREGAME || NetworkMatch.InGameplay()) { PlayerSnapshotToClientMessage item = msg.ReadMessage <PlayerSnapshotToClientMessage>(); NewPlayerSnapshotToClientMessage newItem = new NewPlayerSnapshotToClientMessage { m_num_snapshots = item.m_num_snapshots, m_server_timestamp = 0, // Unused. m_snapshots = item.m_snapshots.Select(m => NewPlayerSnapshot.FromOldSnapshot(m)).ToArray() }; MPClientShipReckoning.AddNewPlayerSnapshot(newItem, true); } return(false); }
// add a AddNewPlayerSnapshot(NewPlayerSnapshotToClientMessage // this should be called as soon as possible after the message arrives // This function adds the message into the ring buffer, and // also implements the time sync algorithm between the message sequence and // the local render time. // // It is safe to be called from an arbitrary thread, as accesses are // guareded by a lock. public static void AddNewPlayerSnapshot(NewPlayerSnapshotToClientMessage msg, bool wasOld = false) { lock (m_last_messages_lock) { if (m_last_messages_ring_count == 0) { // first packet EnqueueToRing(msg); m_last_update_time = Time.time; } else { // next in sequence, as we expected EnqueueToRing(msg, wasOld); // this assumes the server sends 60Hz // during time dilation (timebombs!) this is not true, // it will actually send data packets _worth_ of 16.67ms real time, spread out // to longer intervals such as 24 ms. // However, this is not a problem, since the reference clock we sync to // is Time.time which has the timeScale already applied. // That means the 24ms tick will be seen as 16.67 in Time.time, // and everything cancles itself out nicely m_last_update_time += Time.fixedDeltaTime; } // check if the time base is still plausible float delta = (Time.time - m_last_update_time) / Time.fixedDeltaTime; // in ticks // allow a sliding window to catch up for latency jitter float frameSoftSyncLimit = 2.0f; ///hard-sync if we're off by more than that many physics ticks if (delta < -frameSoftSyncLimit || delta > frameSoftSyncLimit) { // hard resync // Debug.LogFormat("hard resync by {0} frames", delta); m_last_update_time = Time.time; } else { // soft resync float smoothing_factor = 0.1f; m_last_update_time += smoothing_factor * delta * Time.fixedDeltaTime; } } // end lock }
// interpolate a whole NewPlayerSnapshotToClientMessage // interpolate between A and B, t is the relative factor between both // If a player is not in both messages, it will not be in the resulting messages // this is used for generating synthetic snapshot messages in case we detected missing packets private static NewPlayerSnapshotToClientMessage InterpolatePlayerSnapshotMessage(NewPlayerSnapshotToClientMessage A, NewPlayerSnapshotToClientMessage B, float t) { NewPlayerSnapshotToClientMessage C = new NewPlayerSnapshotToClientMessage(); int i, j; C.m_num_snapshots = 0; for (i = 0; i < A.m_num_snapshots; i++) { for (j = 0; j < B.m_num_snapshots; j++) { if (A.m_snapshots[i].m_net_id.Value == B.m_snapshots[j].m_net_id.Value) { InterpolatePlayerSnapshot(ref C.m_snapshots[C.m_num_snapshots++], A.m_snapshots[i], B.m_snapshots[j], t); continue; } } } C.m_server_timestamp = (1.0f - t) * A.m_server_timestamp + t * B.m_server_timestamp; return(C); }
// Called per frame, moves ships along their interpolation/extrapolation motions public static void updatePlayerPositions() { float now = Time.time; // needs to be the same time source we use for m_last_update_time NewPlayerSnapshotToClientMessage msgA = null; // interpolation: start NewPlayerSnapshotToClientMessage msgB = null; // interpolation: end, extrapolation start float interpolate_factor = 0.0f; // interpolation: factor in [0,1] float delta_t = 0.0f; int interpolate_ticks = 0; bool do_interpolation = false; // find out which case we have, and get the relevant snapshot message(s) lock (m_last_messages_lock) { /* * for (int xxx=0; xxx<m_last_messages_ring_count; xxx++) { * Debug.LogFormat("having snapshot from {0} represents {1}", m_last_messages_ring[(m_last_messages_ring_pos_last + 4 - xxx)&3].m_timestamp, m_last_update_time - xxx* Time.fixedDeltaTime); * } */ if (m_last_messages_ring_count < 1) { // we do not have any snapshot messages... return; } // make sure m_last_update_time is up-to-date ResyncTime(); // NOTE: now and m_last_update_time indirectly have timeScale already applied, as they are based on Time.time // we need to adjust just the ping and the mms_ship_max_interpolate_frames offset... // Also note that the server still sends the unscaled velocities. delta_t = now + Time.timeScale * MPClientExtrapolation.GetShipExtrapolationTime() - m_last_update_time; // if we want interpolation, add this as a _negative_ offset // we use delta_t=0 as the base for from which we extrapolate into the future delta_t -= (Menus.mms_lag_compensation_ship_added_lag / 1000f) * Time.timeScale; // it might sound absurd, but after this point, the Time.fixedDeltaTime is correct // and MUST NOT be scaled by timeScale. The data packets do contain 16.67ms of // movement each, we already have taken the time dilation into account above... // time difference in physics ticks float delta_ticks = delta_t / Time.fixedDeltaTime; // the number of frames we need to interpolate into // <= 0 means no interpolation at all, // 1 would mean we use the second most recent and the most recent snapshot, and so on... interpolate_ticks = -(int)Mathf.Floor(delta_ticks); // do we need to do interpolation? do_interpolation = (interpolate_ticks > 0); if (do_interpolation) { // we need interpolate_ticks + 1 elements in the ring buffer // NOTE: in the code below, the index [(m_last_messages_ring_pos_last + 4 - i) &3] // effectively acceses the i-ith most recent element (i starting by 0) // since 4-(i-1) == 4-i+ 1 = 5-i, 5-i references the next older one if (interpolate_ticks < m_last_messages_ring_count) { msgA = m_last_messages_ring[(m_last_messages_ring_pos_last + 4 - interpolate_ticks) & 3]; msgB = m_last_messages_ring[(m_last_messages_ring_pos_last + 5 - interpolate_ticks) & 3]; interpolate_factor = delta_ticks - Mathf.Floor(delta_ticks); } else { // not enough packets received so far // "extrapolate" into the past do_interpolation = false; // get the oldest snapshot we have msgB = m_last_messages_ring[(m_last_messages_ring_pos_last + 5 - m_last_messages_ring_count) & 3]; // offset the time for the extrapolation // delta_t is currently relative to the most recent element we have, // but we need it relative to msgA delta_t += Time.fixedDeltaTime * (m_last_messages_ring_count - 1); } } else { // extrapolation case // use the most recently received snapshot msgB = m_last_messages_ring[m_last_messages_ring_pos_last]; } } // lock m_last_frame_time = now; /* * Debug.LogFormat("At: {0} Setting: {1} IntFrames: {2}, dt: {3}, IntFact {4}",now,Menus.mms_ship_max_interpolate_frames, interpolate_ticks, delta_t, interpolate_factor); * if (interpolate_ticks > 0) { * Debug.LogFormat("Using A from {0}", msgA.m_timestamp); * Debug.LogFormat("Using B from {0}", msgB.m_timestamp); * } else { * Debug.LogFormat("Using B from {0}", msgB.m_timestamp); * } */ // keep statistics m_compensation_sum += delta_t; m_compensation_count++; // NOTE: one can't replace(interpolate_ticks > 0) by do_interpolation here, // because even in the (interpolate_ticks > 0) case the code above could // have reset do_interpolation to false because we technically want // the "extrapolation" into the past thing, but we don't want to count that // as extrapolation... m_compensation_interpol_count += (interpolate_ticks > 0)?1:0; if (Time.time >= m_compensation_last + 5.0 && m_compensation_count > 0) { // Debug.LogFormat("ship lag compensation over last {0} frames: {1}ms / {2} physics ticks, {3} interpolation ({4}%) packets: {5} received / {6} missing / {7} old ignored", // m_compensation_count, 1000.0f* (m_compensation_sum/ m_compensation_count), // (m_compensation_sum/m_compensation_count)/Time.fixedDeltaTime, // m_compensation_interpol_count, // 100.0f*((float)m_compensation_interpol_count/(float)m_compensation_count), // m_received_packets_count, m_missing_packets_count, m_ignored_packets_count); m_compensation_sum = 0.0f; m_compensation_count = 0; m_compensation_interpol_count = 0; m_received_packets_count = 0; m_missing_packets_count = 0; m_ignored_packets_count = 0; m_compensation_last = Time.time; } // actually apply the operation to each player foreach (Player player in Overload.NetworkManager.m_Players) { if (player != null && !player.isLocalPlayer && !player.m_spectator) { // do the actual interpolation or extrapolation, as calculated above if (do_interpolation) { NewPlayerSnapshot A = GetPlayerSnapshot(msgA, player); NewPlayerSnapshot B = GetPlayerSnapshot(msgB, player); if (A != null && B != null) { interpolatePlayer(player, A, B, interpolate_factor); } } else { NewPlayerSnapshot snapshot = GetPlayerSnapshot(msgB, player); if (snapshot != null) { extrapolatePlayer(player, snapshot, delta_t); } } } } }
// add a AddNewPlayerSnapshot(NewPlayerSnapshotToClientMessage // this should be called as soon as possible after the message arrives // This function adds the message into the ring buffer, and // also implements the time sync algorithm between the message sequence and // the local render time. // // It is safe to be called from an arbitrary thread, as accesses are // guareded by a lock. public static void AddNewPlayerSnapshot(NewPlayerSnapshotToClientMessage msg, MPNoPositionCompression.SnapshotVersion version) { lock (m_last_messages_lock) { if (m_last_messages_ring_count == 0) { // first packet EnqueueToRing(msg); m_last_update_time = Time.time; m_unsynced_messages_count = 0; } else { bool estimateVelocities = (version == MPNoPositionCompression.SnapshotVersion.VANILLA); int deltaFrames; if (version != MPNoPositionCompression.SnapshotVersion.VELOCITY_TIMESTAMP) { // we do not have server timestamps, // just assume the packet is the next in sequence deltaFrames = 1; } else { // determine how many frames in the future the new packet is, // relative to the last one we received deltaFrames = (int)((msg.m_server_timestamp - m_last_message_server_time) / Time.fixedDeltaTime + 0.5f); } if (deltaFrames == 1) { // FAST PATH: // next in sequence, as we expected EnqueueToRing(msg, estimateVelocities); m_unsynced_messages_count++; } else if (deltaFrames > 1) { // SLOW PATH: at least one packet is missing // we actually do the creation of missing packets here // once per received new snapshot, so that the per-frame // update code path can stay simple // Debug.LogFormat("detected {0} missing packets", deltaFrames - 1); NewPlayerSnapshotToClientMessage lastMsg = m_last_messages_ring[m_last_messages_ring_pos_last]; if (deltaFrames == 2) { // there is one missing snapshot EnqueueToRing(InterpolatePlayerSnapshotMessage(lastMsg, msg, 0.5f)); EnqueueToRing(msg); } else if (deltaFrames == 3) { // there are two missing snapshots EnqueueToRing(InterpolatePlayerSnapshotMessage(lastMsg, msg, 0.3333f)); EnqueueToRing(InterpolatePlayerSnapshotMessage(lastMsg, msg, 0.6667f)); EnqueueToRing(msg); } else if (deltaFrames == 4) { // there are three missing snapshots EnqueueToRing(InterpolatePlayerSnapshotMessage(lastMsg, msg, 0.25f)); EnqueueToRing(InterpolatePlayerSnapshotMessage(lastMsg, msg, 0.5f)); EnqueueToRing(InterpolatePlayerSnapshotMessage(lastMsg, msg, 0.75f)); EnqueueToRing(msg); } else { // there are more than 3 missing snapshots, // just take the completely new data in EnqueueToRing(ExtrapolatePlayerSnapshotMessage(msg, -3.0f * Time.fixedDeltaTime)); EnqueueToRing(ExtrapolatePlayerSnapshotMessage(msg, -2.0f * Time.fixedDeltaTime)); EnqueueToRing(ExtrapolatePlayerSnapshotMessage(msg, -1.0f * Time.fixedDeltaTime)); EnqueueToRing(msg); } m_unsynced_messages_count += deltaFrames; m_missing_packets_count += (deltaFrames - 1); } else if (deltaFrames < -180) { // WEIRD PATH: message from the past older than 3 seconds // this means something completely weird is going on on the network // or on the server, and we better completely re-sync with the server // Debug.LogFormat("detected message {0} frames from the past, FULL RESYNC!", -deltaFrames); ClearRing(); EnqueueToRing(msg, false); m_last_update_time = Time.time; m_unsynced_messages_count = 0; } else { // Debug.LogFormat("detected old / duplicated message {0} frames from the past", -deltaFrames); // OLD / DUPLICATED messages: these are simply ignored // If it is a true duplicate, it is worthless, and if we got messages // out of order, we have synthesized the missing packet already with // the data we had, so this is not an exact duplicate, but it is // useless now anyway. // // With the "reliable" UDP connection unity offers, this path should // never be taken (and I never saw it happen), but this code // is written to deal with any input whatsoever as good as possible. // Note that we might also experiment with using a completely // unreliable connection in the future, and then, this code path // becomes essential. m_ignored_packets_count++; } } m_received_packets_count++; m_last_message_time = Time.time; m_last_message_server_time = msg.m_server_timestamp; } // end lock }