void UpdateClient() { // client authority, and local player (= allowed to move myself)? if (IsClientWithAuthority) { // send to server each 'sendInterval' // NetworkTime.localTime for double precision until Unity has it too // // IMPORTANT: // snapshot interpolation requires constant sending. // DO NOT only send if position changed. for example: // --- // * client sends first position at t=0 // * ... 10s later ... // * client moves again, sends second position at t=10 // --- // * server gets first position at t=0 // * server gets second position at t=10 // * server moves from first to second within a time of 10s // => would be a super slow move, instead of a wait & move. // // IMPORTANT: // DO NOT send nulls if not changed 'since last send' either. we // send unreliable and don't know which 'last send' the other end // received successfully. if (NetworkTime.localTime >= lastClientSendTime + sendInterval) { // send snapshot without timestamp. // receiver gets it from batch timestamp to save bandwidth. NTSnapshot snapshot = ConstructSnapshot(); CmdClientToServerSync( // only sync what the user wants to sync syncPosition ? snapshot.position : new Vector3?(), syncRotation? snapshot.rotation : new Quaternion?(), syncScale ? snapshot.scale : new Vector3?() ); lastClientSendTime = NetworkTime.localTime; } } // for all other clients (and for local player if !authority), // we need to apply snapshots from the buffer else { // compute snapshot interpolation & apply if any was spit out // TODO we don't have Time.deltaTime double yet. float is fine. if (SnapshotInterpolation.Compute( NetworkTime.localTime, Time.deltaTime, ref clientInterpolationTime, bufferTime, clientBuffer, catchupThreshold, catchupMultiplier, Interpolate, out NTSnapshot computed)) { NTSnapshot start = clientBuffer.Values[0]; NTSnapshot goal = clientBuffer.Values[1]; ApplySnapshot(start, goal, computed); } } }
protected virtual void DrawGizmos(SortedList <double, NTSnapshot> buffer) { // only draw if we have at least two entries if (buffer.Count < 2) { return; } // calcluate threshold for 'old enough' snapshots double threshold = NetworkTime.localTime - bufferTime; Color oldEnoughColor = new Color(0, 1, 0, 0.5f); Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); // draw the whole buffer for easier debugging. // it's worth seeing how much we have buffered ahead already for (int i = 0; i < buffer.Count; ++i) { // color depends on if old enough or not NTSnapshot entry = buffer.Values[i]; bool oldEnough = entry.localTimestamp <= threshold; Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; Gizmos.DrawCube(entry.position, Vector3.one); } // extra: lines between start<->position<->goal Gizmos.color = Color.green; Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position); Gizmos.color = Color.white; Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position); }
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range. protected virtual bool CompareSnapshots(NTSnapshot currentSnapshot) { positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) >= positionSensitivity * positionSensitivity; rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) >= rotationSensitivity; scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) >= scaleSensitivity * scaleSensitivity; return(!positionChanged && !rotationChanged && !scaleChanged); }
// local authority client sends sync message to server for broadcasting protected virtual void OnClientToServerSync(Vector3?position, Quaternion?rotation, Vector3?scale) { // only apply if in client authority mode if (!clientAuthority) { return; } // protect against ever growing buffer size attacks if (serverBuffer.Count >= bufferSizeLimit) { return; } // only player owned objects (with a connection) can send to // server. we can get the timestamp from the connection. double timestamp = connectionToClient.remoteTimeStamp; // position, rotation, scale can have no value if same as last time. // saves bandwidth. // but we still need to feed it to snapshot interpolation. we can't // just have gaps in there if nothing has changed. for example, if // client sends snapshot at t=0 // client sends nothing for 10s because not moved // client sends snapshot at t=10 // then the server would assume that it's one super slow move and // replay it for 10 seconds. if (!position.HasValue) { position = targetComponent.localPosition; } if (!rotation.HasValue) { rotation = targetComponent.localRotation; } if (!scale.HasValue) { scale = targetComponent.localScale; } // construct snapshot with batch timestamp to save bandwidth NTSnapshot snapshot = new NTSnapshot( timestamp, NetworkTime.localTime, position.Value, rotation.Value, scale.Value ); // add to buffer (or drop if older than first element) SnapshotInterpolation.InsertIfNewEnough(snapshot, serverBuffer); }
public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t) { // NOTE: // Vector3 & Quaternion components are float anyway, so we can // keep using the functions with 't' as float instead of double. return(new NTSnapshot( // interpolated snapshot is applied directly. don't need timestamps. 0, 0, // lerp position/rotation/scale unclamped in case we ever need // to extrapolate. atm SnapshotInterpolation never does. Vector3.LerpUnclamped(from.position, to.position, (float)t), // IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86. // SlerpUnclamped(0, 60, 1.5) extrapolates to 90! // (0, 90, 1.5) is even worse. for Lerp. // => Slerp works way better for our euler angles. Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t), Vector3.LerpUnclamped(from.scale, to.scale, (float)t) )); }
// apply a snapshot to the Transform. // -> start, end, interpolated are all passed in caes they are needed // -> a regular game would apply the 'interpolated' snapshot // -> a board game might want to jump to 'goal' directly // (it's easier to always interpolate and then apply selectively, // instead of manually interpolating x, y, z, ... depending on flags) // => internal for testing // // NOTE: stuck detection is unnecessary here. // we always set transform.position anyway, we can't get stuck. protected virtual void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated) { // local position/rotation for VR support // // if syncPosition/Rotation/Scale is disabled then we received nulls // -> current position/rotation/scale would've been added as snapshot // -> we still interpolated // -> but simply don't apply it. if the user doesn't want to sync // scale, then we should not touch scale etc. if (syncPosition) { targetComponent.localPosition = interpolatePosition ? interpolated.position : goal.position; } if (syncRotation) { targetComponent.localRotation = interpolateRotation ? interpolated.rotation : goal.rotation; } if (syncScale) { targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale; } }
// update ////////////////////////////////////////////////////////////// void UpdateServer() { // broadcast to all clients each 'sendInterval' // (client with authority will drop the rpc) // NetworkTime.localTime for double precision until Unity has it too // // IMPORTANT: // snapshot interpolation requires constant sending. // DO NOT only send if position changed. for example: // --- // * client sends first position at t=0 // * ... 10s later ... // * client moves again, sends second position at t=10 // --- // * server gets first position at t=0 // * server gets second position at t=10 // * server moves from first to second within a time of 10s // => would be a super slow move, instead of a wait & move. // // IMPORTANT: // DO NOT send nulls if not changed 'since last send' either. we // send unreliable and don't know which 'last send' the other end // received successfully. // // Checks to ensure server only sends snapshots if object is // on server authority(!clientAuthority) mode because on client // authority mode snapshots are broadcasted right after the authoritative // client updates server in the command function(see above), OR, // since host does not send anything to update the server, any client // authoritative movement done by the host will have to be broadcasted // here by checking IsClientWithAuthority. if (NetworkTime.localTime >= lastServerSendTime + sendInterval && (!clientAuthority || IsClientWithAuthority)) { // send snapshot without timestamp. // receiver gets it from batch timestamp to save bandwidth. NTSnapshot snapshot = ConstructSnapshot(); RpcServerToClientSync( // only sync what the user wants to sync syncPosition ? snapshot.position : new Vector3?(), syncRotation? snapshot.rotation : new Quaternion?(), syncScale ? snapshot.scale : new Vector3?() ); lastServerSendTime = NetworkTime.localTime; } // apply buffered snapshots IF client authority // -> in server authority, server moves the object // so no need to apply any snapshots there. // -> don't apply for host mode player objects either, even if in // client authority mode. if it doesn't go over the network, // then we don't need to do anything. if (clientAuthority && !hasAuthority) { // compute snapshot interpolation & apply if any was spit out // TODO we don't have Time.deltaTime double yet. float is fine. if (SnapshotInterpolation.Compute( NetworkTime.localTime, Time.deltaTime, ref serverInterpolationTime, bufferTime, serverBuffer, catchupThreshold, catchupMultiplier, Interpolate, out NTSnapshot computed)) { NTSnapshot start = serverBuffer.Values[0]; NTSnapshot goal = serverBuffer.Values[1]; ApplySnapshot(start, goal, computed); } } }
// server broadcasts sync message to all clients protected virtual void OnServerToClientSync(Vector3?position, Quaternion?rotation, Vector3?scale) { // in host mode, the server sends rpcs to all clients. // the host client itself will receive them too. // -> host server is always the source of truth // -> we can ignore any rpc on the host client // => otherwise host objects would have ever growing clientBuffers // (rpc goes to clients. if isServer is true too then we are host) if (isServer) { return; } // don't apply for local player with authority if (IsClientWithAuthority) { return; } // protect against ever growing buffer size attacks if (clientBuffer.Count >= bufferSizeLimit) { return; } // on the client, we receive rpcs for all entities. // not all of them have a connectionToServer. // but all of them go through NetworkClient.connection. // we can get the timestamp from there. double timestamp = NetworkClient.connection.remoteTimeStamp; // position, rotation, scale can have no value if same as last time. // saves bandwidth. // but we still need to feed it to snapshot interpolation. we can't // just have gaps in there if nothing has changed. for example, if // client sends snapshot at t=0 // client sends nothing for 10s because not moved // client sends snapshot at t=10 // then the server would assume that it's one super slow move and // replay it for 10 seconds. if (!position.HasValue) { position = targetComponent.localPosition; } if (!rotation.HasValue) { rotation = targetComponent.localRotation; } if (!scale.HasValue) { scale = targetComponent.localScale; } // construct snapshot with batch timestamp to save bandwidth NTSnapshot snapshot = new NTSnapshot( timestamp, NetworkTime.localTime, position.Value, rotation.Value, scale.Value ); // add to buffer (or drop if older than first element) SnapshotInterpolation.InsertIfNewEnough(snapshot, clientBuffer); }
void UpdateClient() { // client authority, and local player (= allowed to move myself)? if (IsClientWithAuthority) { // https://github.com/vis2k/Mirror/pull/2992/ if (!NetworkClient.ready) { return; } // send to server each 'sendInterval' // NetworkTime.localTime for double precision until Unity has it too // // IMPORTANT: // snapshot interpolation requires constant sending. // DO NOT only send if position changed. for example: // --- // * client sends first position at t=0 // * ... 10s later ... // * client moves again, sends second position at t=10 // --- // * server gets first position at t=0 // * server gets second position at t=10 // * server moves from first to second within a time of 10s // => would be a super slow move, instead of a wait & move. // // IMPORTANT: // DO NOT send nulls if not changed 'since last send' either. we // send unreliable and don't know which 'last send' the other end // received successfully. if (NetworkTime.localTime >= lastClientSendTime + sendInterval) { // send snapshot without timestamp. // receiver gets it from batch timestamp to save bandwidth. NTSnapshot snapshot = ConstructSnapshot(); #if onlySyncOnChange_BANDWIDTH_SAVING cachedSnapshotComparison = CompareSnapshots(snapshot); if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } #endif #if onlySyncOnChange_BANDWIDTH_SAVING CmdClientToServerSync( // only sync what the user wants to sync syncPosition && positionChanged ? snapshot.position : default(Vector3?), syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), syncScale && scaleChanged ? snapshot.scale : default(Vector3?) ); #else CmdClientToServerSync( // only sync what the user wants to sync syncPosition ? snapshot.position : default(Vector3?), syncRotation ? snapshot.rotation : default(Quaternion?), syncScale ? snapshot.scale : default(Vector3?) ); #endif lastClientSendTime = NetworkTime.localTime; #if onlySyncOnChange_BANDWIDTH_SAVING if (cachedSnapshotComparison) { hasSentUnchangedPosition = true; } else { hasSentUnchangedPosition = false; lastSnapshot = snapshot; } #endif } } // for all other clients (and for local player if !authority), // we need to apply snapshots from the buffer else { // compute snapshot interpolation & apply if any was spit out // TODO we don't have Time.deltaTime double yet. float is fine. if (SnapshotInterpolation.Compute( NetworkTime.localTime, Time.deltaTime, ref clientInterpolationTime, bufferTime, clientBuffer, catchupThreshold, catchupMultiplier, Interpolate, out NTSnapshot computed)) { NTSnapshot start = clientBuffer.Values[0]; NTSnapshot goal = clientBuffer.Values[1]; ApplySnapshot(start, goal, computed); } } }