From 07956819c2040b0330c20c20c2c7e7a305c5799b Mon Sep 17 00:00:00 2001 From: miwarnec Date: Tue, 29 Oct 2024 21:55:37 +0100 Subject: [PATCH 01/14] hybrid nt --- .../NetworkTransformHybrid2022.cs | 1123 +++++++++++++++++ .../NetworkTransformHybrid2022.cs.meta | 11 + 2 files changed, 1134 insertions(+) create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs.meta diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs new file mode 100644 index 000000000..b583bf617 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -0,0 +1,1123 @@ +// Quake NetworkTransform based on 2022 NetworkTransformUnreliable. +// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ +// Quake: https://www.jfedor.org/quake3/ +// +// Base class for NetworkTransform and NetworkTransformChild. +// => simple unreliable sync without any interpolation for now. +// => which means we don't need teleport detection either +// +// several functions are virtual in case someone needs to modify a part. +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform Hybrid")] + public class NetworkTransformHybrid2022 : NetworkBehaviour + { + // target transform to sync. can be on a child. + [Header("Target")] + [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] + public Transform target; + + // TODO SyncDirection { ClientToServer, ServerToClient } is easier? + [Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")] + [Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually. + [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] + public bool clientAuthority; + + // Is this a client with authority over this transform? + // This component could be on the player object or any object that has been assigned authority to this client. + protected bool IsClientWithAuthority => isClient && authority; + + [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")] + public int bufferSizeLimit = 64; + internal SortedList clientSnapshots = new SortedList(); + internal SortedList serverSnapshots = new SortedList(); + + // CUSTOM CHANGE: bring back sendRate. this will probably be ported to Mirror. + // TODO but use built in syncInterval instead of the extra field here! + [Header("Synchronization")] + [Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")] + public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333' + public float sendInterval => 1f / sendRate; + // END CUSTOM CHANGE + + [Tooltip("Ocassionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] + public int baselineRate = 1; + public float baselineInterval => baselineRate < int.MaxValue ? 1f / baselineRate : 0; // for 1 Hz, that's 1000ms + double lastBaselineTime; + double lastDeltaTime; + + // delta compression needs to remember 'last' to compress against. + // this is from reliable full state serializations, not from last + // unreliable delta since that isn't guaranteed to be delivered. + byte lastSerializedBaselineTick = 0; + Vector3 lastSerializedBaselinePosition = Vector3.zero; + Quaternion lastSerializedBaselineRotation = Quaternion.identity; + + // save last deserialized baseline to delta decompress against + byte lastDeserializedBaselineTick = 0; + Vector3 lastDeserializedBaselinePosition = Vector3.zero; + Quaternion lastDeserializedBaselineRotation = Quaternion.identity; + + // only sync when changed hack ///////////////////////////////////////// + [Header("Sync Only If Changed")] + [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] + public bool onlySyncOnChange = true; + + // sensitivity is for changed-detection, + // this is != precision, which is for quantization and delta compression. + [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] + public float positionSensitivity = 0.01f; + public float rotationSensitivity = 0.01f; + // public float scaleSensitivity = 0.01f; + + [Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast paced games since it doubles bandwidth costs.")] + public bool unreliableRedundancy = false; + + [Tooltip("When sending a reliable baseline, should we also send an unreliable delta or rely on the reliable baseline to arrive in a similar time?")] + public bool baselineIsDelta = true; + + // selective sync ////////////////////////////////////////////////////// + [Header("Selective Sync & interpolation")] + public bool syncPosition = true; + public bool syncRotation = true; + // public bool syncScale = false; // rarely used. disabled for perf so we can rely on transform.GetPositionAndRotation. + + // BEGIN CUSTOM CHANGE ///////////////////////////////////////////////// + // TODO rename to avoid double negative + public bool disableSendingThisToClients = false; + // END CUSTOM CHANGE /////////////////////////////////////////////////// + + // debugging /////////////////////////////////////////////////////////// + [Header("Debug")] + public bool showGizmos; + public bool showOverlay; + public Color overlayColor = new Color(0, 0, 0, 0.5f); + + // caching ///////////////////////////////////////////////////////////// + // squared values for faster distance checks + // float positionPrecisionSqr; + // float scalePrecisionSqr; + + // dedicated writer to avoid Pool.Get calls. NT is in hot path. + readonly NetworkWriter writer = new NetworkWriter(); + + // initialization ////////////////////////////////////////////////////// + // make sure to call this when inheriting too! + protected virtual void Awake() {} + + protected override void OnValidate() + { + base.OnValidate(); + + // set target to self if none yet + if (target == null) target = transform; + + // use sendRate instead of syncInterval for now + syncInterval = 0; + + // cache squared precisions + // positionPrecisionSqr = positionPrecision * positionPrecision; + // scalePrecisionSqr = scalePrecision * scalePrecision; + + // obsolete clientAuthority compatibility: + // if it was used, then set the new SyncDirection automatically. + // if it wasn't used, then don't touch syncDirection. + #pragma warning disable CS0618 + if (clientAuthority) + { + syncDirection = SyncDirection.ClientToServer; + Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead."); + } + #pragma warning restore CS0618 + } + + // snapshot functions ////////////////////////////////////////////////// + // construct a snapshot of the current state + // => internal for testing + protected virtual TransformSnapshot ConstructSnapshot() + { + // perf + target.GetLocalPositionAndRotation(out Vector3 localPosition, out Quaternion localRotation); + + // NetworkTime.localTime for double precision until Unity has it too + return new TransformSnapshot( + // our local time is what the other end uses as remote time + Time.timeAsDouble, + // the other end fills out local time itself + 0, + localPosition, // target.localPosition, + localRotation, // target.localRotation, + Vector3.zero // target.localScale + ); + } + + // 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(TransformSnapshot 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) target.localPosition = interpolated.position; + if (syncRotation) target.localRotation = interpolated.rotation; + // if (syncScale) target.localScale = interpolated.scale; + } + + // check if position / rotation / scale changed since last _full reliable_ sync. + // squared comparisons for performance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool Changed(Vector3 currentPosition, Quaternion currentRotation)//, Vector3 currentScale) + { + if (syncPosition) + { + float positionDelta = Vector3.Distance(currentPosition, lastSerializedBaselinePosition); + if (positionDelta >= positionSensitivity) + // float positionChange = (currentPosition - lastPosition).sqrMagnitude; + // if (positionChange >= positionPrecisionSqr) + { + return true; + } + } + + if (syncRotation) + { + float rotationDelta = Quaternion.Angle(lastSerializedBaselineRotation, currentRotation); + if (rotationDelta >= rotationSensitivity) + { + return true; + } + } + + // if (syncScale && Vector3.Distance(last.scale, current.scale) >= scalePrecision) + // if (syncScale && (current.scale - last.scale).sqrMagnitude >= scalePrecisionSqr) + // return true; + + return false; + } + + // cmd baseline //////////////////////////////////////////////////////// + [Command(channel = Channels.Reliable)] // reliable baseline + void CmdClientToServerBaseline_PositionRotation(byte baselineTick, Vector3 position, Quaternion rotation) + { + lastDeserializedBaselineTick = baselineTick; + lastDeserializedBaselinePosition = position; + lastDeserializedBaselineRotation = rotation; + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnClientToServerDeltaSync(baselineTick, position, rotation);//, scale); + } + + [Command(channel = Channels.Reliable)] // reliable baseline + void CmdClientToServerBaseline_Position(byte baselineTick, Vector3 position) + { + lastDeserializedBaselineTick = baselineTick; + lastDeserializedBaselinePosition = position; + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnClientToServerDeltaSync(baselineTick, position, Quaternion.identity);//, scale); + } + + [Command(channel = Channels.Reliable)] // reliable baseline + void CmdClientToServerBaseline_Rotation(byte baselineTick, Quaternion rotation) + { + lastDeserializedBaselineTick = baselineTick; + lastDeserializedBaselineRotation = rotation; + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnClientToServerDeltaSync(baselineTick, Vector3.zero, rotation);//, scale); + } + + // cmd delta /////////////////////////////////////////////////////////// + [Command(channel = Channels.Unreliable)] // unreliable delta + void CmdClientToServerDelta_Position(byte baselineTick, Vector3 position) + { + // Debug.Log($"[{name}] server received delta for baseline #{lastDeserializedBaselineTick}"); + OnClientToServerDeltaSync(baselineTick, position, Quaternion.identity);//, scale); + } + + [Command(channel = Channels.Unreliable)] // unreliable delta + void CmdClientToServerDelta_Rotation(byte baselineTick, Quaternion rotation) + { + // Debug.Log($"[{name}] server received delta for baseline #{lastDeserializedBaselineTick}"); + OnClientToServerDeltaSync(baselineTick, Vector3.zero, rotation);//, scale); + } + + [Command(channel = Channels.Unreliable)] // unreliable delta + void CmdClientToServerDelta_PositionRotation(byte baselineTick, Vector3 position, Quaternion rotation) + { + // Debug.Log($"[{name}] server received delta for baseline #{lastDeserializedBaselineTick}"); + OnClientToServerDeltaSync(baselineTick, position, rotation);//, scale); + } + + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerDeltaSync(byte baselineTick, Vector3? position, Quaternion? rotation)//, Vector3? scale) + { + // ensure this delta is for our last known baseline. + // we should never apply a delta on top of a wrong baseline. + if (baselineTick != lastDeserializedBaselineTick) + { + // this can happen if unreliable arrives before reliable etc. + // no need to log this except when debugging. + // Debug.Log($"[{name}] Server: received delta for wrong baseline #{baselineTick} from: {connectionToClient}. Last was {lastDeserializedBaselineTick}. Ignoring."); + return; + } + + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) 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 = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].position : target.localPosition; + if (!rotation.HasValue) rotation = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].rotation : target.localRotation; + // if (!scale.HasValue) scale = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].scale : target.localScale; + + // insert transform snapshot + SnapshotInterpolation.InsertIfNotExists( + serverSnapshots, + bufferSizeLimit, + new TransformSnapshot( + timestamp, // arrival remote timestamp. NOT remote time. + Time.timeAsDouble, + position.Value, + rotation.Value, + Vector3.one // scale + )); + } + + // rpc baseline //////////////////////////////////////////////////////// + [ClientRpc(channel = Channels.Reliable)] // reliable baseline + void RpcServerToClientBaseline_PositionRotation(byte baselineTick, Vector3 position, Quaternion rotation) + { + // baseline is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + // save last deserialized baseline tick number to compare deltas against + lastDeserializedBaselineTick = baselineTick; + lastDeserializedBaselinePosition = position; + lastDeserializedBaselineRotation = rotation; + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnServerToClientDeltaSync(baselineTick, position, rotation);//, Vector3.zero);//, scale); + } + + [ClientRpc(channel = Channels.Reliable)] // reliable baseline + void RpcServerToClientBaseline_Position(byte baselineTick, Vector3 position) + { + // baseline is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + // save last deserialized baseline tick number to compare deltas against + lastDeserializedBaselineTick = baselineTick; + lastDeserializedBaselinePosition = position; + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnServerToClientDeltaSync(baselineTick, position, Quaternion.identity);//, Vector3.zero);//, scale); + } + + [ClientRpc(channel = Channels.Reliable)] // reliable baseline + void RpcServerToClientBaseline_Rotation(byte baselineTick, Quaternion rotation) + { + // baseline is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + // save last deserialized baseline tick number to compare deltas against + lastDeserializedBaselineTick = baselineTick; + lastDeserializedBaselineRotation = rotation; + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnServerToClientDeltaSync(baselineTick, Vector3.zero, rotation);//, Vector3.zero);//, scale); + } + + // rpc delta /////////////////////////////////////////////////////////// + [ClientRpc(channel = Channels.Unreliable)] // unreliable delta + void RpcServerToClientDelta_PositionRotation(byte baselineTick, Vector3 position, Quaternion rotation) + { + // delta is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + OnServerToClientDeltaSync(baselineTick, position, rotation);//, scale); + } + + + [ClientRpc(channel = Channels.Unreliable)] // unreliable delta + void RpcServerToClientDelta_Position(byte baselineTick, Vector3 position) + { + // delta is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + OnServerToClientDeltaSync(baselineTick, position, Quaternion.identity);//, scale); + } + + + [ClientRpc(channel = Channels.Unreliable)] // unreliable delta + void RpcServerToClientDelta_Rotation(byte baselineTick, Quaternion rotation) + { + // delta is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + OnServerToClientDeltaSync(baselineTick, Vector3.zero, rotation);//, scale); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientDeltaSync(byte baselineTick, Vector3 position, Quaternion rotation)//, Vector3 scale) + { + // ensure this delta is for our last known baseline. + // we should never apply a delta on top of a wrong baseline. + if (baselineTick != lastDeserializedBaselineTick) + { + // this can happen if unreliable arrives before reliable etc. + // no need to log this except when debugging. + // Debug.Log($"[{name}] Client: received delta for wrong baseline #{baselineTick}. Last was {lastDeserializedBaselineTick}. Ignoring."); + return; + } + + // 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; + + // Debug.Log($"[{name}] Client: received delta for baseline #{baselineTick}"); + + // 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 (!syncPosition) position = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].position : target.localPosition; + // if (!syncRotation) rotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : target.localRotation; + // if (!syncScale) scale = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].scale : target.localScale; + + // insert snapshot + SnapshotInterpolation.InsertIfNotExists( + clientSnapshots, + bufferSizeLimit, + new TransformSnapshot( + timestamp, // arrival remote timestamp. NOT remote time. + Time.timeAsDouble, + position, + rotation, + Vector3.one // scale + )); + } + + // update server /////////////////////////////////////////////////////// + bool baselineDirty = true; + void UpdateServerBaseline(double localTime) + { + // send a reliable baseline every 1 Hz + if (localTime >= lastBaselineTime + baselineInterval) + { + // Debug.Log($"UpdateServerBaseline for {name}"); + + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + + // only send a new reliable baseline if changed since last time + // check if changed (unless that feature is disabled). + // baseline is guaranteed to be delivered over reliable. + // here is the only place where we can check for changes. + if (!onlySyncOnChange || Changed(position, rotation)) //snapshot)) + { + // reliable just changed. keep sending deltas until it's unchanged again. + baselineDirty = true; + + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + if (syncPosition && syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + RpcServerToClientBaseline_PositionRotation(frameCount, position, rotation); + } + else if (syncPosition) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + RpcServerToClientBaseline_Position(frameCount, position); + } + else if (syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + RpcServerToClientBaseline_Rotation(frameCount, rotation); + } + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + lastSerializedBaselinePosition = position; + lastSerializedBaselineRotation = rotation; + + // perf. & bandwidth optimization: + // send a delta right after baseline to avoid potential head of + // line blocking, or skip the delta whenever we sent reliable? + // for example: + // 1 Hz baseline + // 10 Hz delta + // => 11 Hz total if we still send delta after reliable + // => 10 Hz total if we skip delta after reliable + // in that case, skip next delta by simply resetting last delta sync's time. + if (baselineIsDelta) lastDeltaTime = localTime; + } + // indicate that we should stop sending deltas now + else baselineDirty = false; + } + } + + void UpdateServerDelta(double localTime) + { + // 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. + // TODO send same time that NetworkServer sends time snapshot? + + // only sync on change: + // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. + // if baseline is dirty, send unreliables every sendInterval until baseline is not dirty anymore. + if (onlySyncOnChange && !baselineDirty) return; + + if (localTime >= lastDeltaTime + sendInterval) // CUSTOM CHANGE: allow custom sendRate + sendInterval again + { + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + if (syncPosition && syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + // unreliable redundancy to make up for potential message drops + RpcServerToClientDelta_PositionRotation(lastSerializedBaselineTick, position, rotation); + if (unreliableRedundancy) + RpcServerToClientDelta_PositionRotation(lastSerializedBaselineTick, position, rotation); + } + else if (syncPosition) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + // unreliable redundancy to make up for potential message drops + RpcServerToClientDelta_Position(lastSerializedBaselineTick, position); + if (unreliableRedundancy) + RpcServerToClientDelta_Position(lastSerializedBaselineTick, position); + } + else if (syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + // unreliable redundancy to make up for potential message drops + RpcServerToClientDelta_Rotation(lastSerializedBaselineTick, rotation); + if (unreliableRedundancy) + RpcServerToClientDelta_Rotation(lastSerializedBaselineTick, rotation); + } + + + lastDeltaTime = localTime; + } + } + + void UpdateServerInterpolation() + { + // 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 (syncDirection == SyncDirection.ClientToServer && !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + // CUSTOM CHANGE: allow for custom sendRate+sendInterval again. + // for example, if the object is moving @ 1 Hz, always put it back by 1s. + // that's how we still get smooth movement even with a global timeline. + connectionToClient.remoteTimeline - sendInterval, + // END CUSTOM CHANGE + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + ApplySnapshot(computed); + } + } + } + + void UpdateServer() + { + // server broadcasts all objects all the time. + // -> not just ServerToClient: ClientToServer need to be broadcast to others too + + // perf: only grab NetworkTime.localTime property once. + double localTime = NetworkTime.localTime; + + // should we broadcast at all? + if (!disableSendingThisToClients) // CUSTOM CHANGE: see comment at definition + { + UpdateServerBaseline(localTime); + UpdateServerDelta(localTime); + } + + // interpolate remote clients + UpdateServerInterpolation(); + } + + // update client /////////////////////////////////////////////////////// + void UpdateClientBaseline(double localTime) + { + // send a reliable baseline every 1 Hz + if (localTime >= lastBaselineTime + baselineInterval) + { + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + + // only send a new reliable baseline if changed since last time + // check if changed (unless that feature is disabled). + // baseline is guaranteed to be delivered over reliable. + // here is the only place where we can check for changes. + if (!onlySyncOnChange || Changed(position, rotation)) //snapshot)) + { + // reliable just changed. keep sending deltas until it's unchanged again. + baselineDirty = true; + + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + if (syncPosition && syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + CmdClientToServerBaseline_PositionRotation(frameCount, position, rotation); + } + else if (syncPosition) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + CmdClientToServerBaseline_Position(frameCount, position); + } + else if (syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + CmdClientToServerBaseline_Rotation(frameCount, rotation); + } + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + lastSerializedBaselinePosition = position; + lastSerializedBaselineRotation = rotation; + + // perf. & bandwidth optimization: + // send a delta right after baseline to avoid potential head of + // line blocking, or skip the delta whenever we sent reliable? + // for example: + // 1 Hz baseline + // 10 Hz delta + // => 11 Hz total if we still send delta after reliable + // => 10 Hz total if we skip delta after reliable + // in that case, skip next delta by simply resetting last delta sync's time. + if (baselineIsDelta) lastDeltaTime = localTime; + } + // indicate that we should stop sending deltas now + else baselineDirty = false; + } + } + + void UpdateClientDelta(double localTime) + { + // only sync on change: + // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. + // if baseline is dirty, send unreliables every sendInterval until baseline is not dirty anymore. + if (onlySyncOnChange && !baselineDirty) 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 (localTime >= lastDeltaTime + sendInterval) // CUSTOM CHANGE: allow custom sendRate + sendInterval again + { + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + if (syncPosition && syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + // unreliable redundancy to make up for potential message drops + CmdClientToServerDelta_PositionRotation(lastSerializedBaselineTick, position, rotation); + if (unreliableRedundancy) + CmdClientToServerDelta_PositionRotation(lastSerializedBaselineTick, position, rotation); + + } + else if (syncPosition) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + // unreliable redundancy to make up for potential message drops + CmdClientToServerDelta_Position(lastSerializedBaselineTick, position); + if (unreliableRedundancy) + CmdClientToServerDelta_Position(lastSerializedBaselineTick, position); + } + else if (syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + // unreliable redundancy to make up for potential message drops + CmdClientToServerDelta_Rotation(lastSerializedBaselineTick, rotation); + if (unreliableRedundancy) + CmdClientToServerDelta_Rotation(lastSerializedBaselineTick, rotation); + } + + lastDeltaTime = localTime; + } + } + + void UpdateClientInterpolation() + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + // CUSTOM CHANGE: allow for custom sendRate+sendInterval again. + // for example, if the object is moving @ 1 Hz, always put it back by 1s. + // that's how we still get smooth movement even with a global timeline. + NetworkTime.time - sendInterval, // == NetworkClient.localTimeline from snapshot interpolation + // END CUSTOM CHANGE + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + ApplySnapshot(computed); + } + } + + void UpdateClient() + { + // client authority, and local player (= allowed to move myself)? + if (IsClientWithAuthority) + { + // https://github.com/vis2k/Mirror/pull/2992/ + if (!NetworkClient.ready) return; + + // perf: only grab NetworkTime.localTime property once. + double localTime = NetworkTime.localTime; + + UpdateClientBaseline(localTime); + UpdateClientDelta(localTime); + } + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer + else + { + UpdateClientInterpolation(); + } + } + + void Update() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // reset any in-progress interpolation & buffers + Reset(); + + // set the new position. + // interpolation will automatically continue. + target.position = destination; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) + { + // reset any in-progress interpolation & buffers + Reset(); + + // set the new position. + // interpolation will automatically continue. + target.position = destination; + target.rotation = rotation; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + // server->client teleport to force position without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [ClientRpc] + public void RpcTeleport(Vector3 destination) + { + // NOTE: even in client authority mode, the server is always allowed + // to teleport the player. for example: + // * CmdEnterPortal() might teleport the player + // * Some people use client authority with server sided checks + // so the server should be able to reset position if needed. + + // TODO what about host mode? + OnTeleport(destination); + } + + // server->client teleport to force position and rotation without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [ClientRpc] + public void RpcTeleport(Vector3 destination, Quaternion rotation) + { + // NOTE: even in client authority mode, the server is always allowed + // to teleport the player. for example: + // * CmdEnterPortal() might teleport the player + // * Some people use client authority with server sided checks + // so the server should be able to reset position if needed. + + // TODO what about host mode? + OnTeleport(destination, rotation); + } + + // client->server teleport to force position without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [Command] + public void CmdTeleport(Vector3 destination) + { + // client can only teleport objects that it has authority over. + if (syncDirection != SyncDirection.ClientToServer) return; + + // TODO what about host mode? + OnTeleport(destination); + + // if a client teleports, we need to broadcast to everyone else too + // TODO the teleported client should ignore the rpc though. + // otherwise if it already moved again after teleporting, + // the rpc would come a little bit later and reset it once. + // TODO or not? if client ONLY calls Teleport(pos), the position + // would only be set after the rpc. unless the client calls + // BOTH Teleport(pos) and target.position=pos + RpcTeleport(destination); + } + + // client->server teleport to force position and rotation without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [Command] + public void CmdTeleport(Vector3 destination, Quaternion rotation) + { + // client can only teleport objects that it has authority over. + if (syncDirection != SyncDirection.ClientToServer) return; + + // TODO what about host mode? + OnTeleport(destination, rotation); + + // if a client teleports, we need to broadcast to everyone else too + // TODO the teleported client should ignore the rpc though. + // otherwise if it already moved again after teleporting, + // the rpc would come a little bit later and reset it once. + // TODO or not? if client ONLY calls Teleport(pos), the position + // would only be set after the rpc. unless the client calls + // BOTH Teleport(pos) and target.position=pos + RpcTeleport(destination, rotation); + } + + [Server] + public void ServerTeleport(Vector3 destination, Quaternion rotation) + { + OnTeleport(destination, rotation); + RpcTeleport(destination, rotation); + } + + public virtual void Reset() + { + // disabled objects aren't updated anymore. + // so let's clear the buffers. + serverSnapshots.Clear(); + clientSnapshots.Clear(); + + // reset baseline + lastSerializedBaselineTick = 0; + lastSerializedBaselinePosition = Vector3.zero; + lastSerializedBaselineRotation = Quaternion.identity; + baselineDirty = true; + + lastDeserializedBaselineTick = 0; + lastDeserializedBaselinePosition = Vector3.zero; + lastDeserializedBaselineRotation = Quaternion.identity; + + // Debug.Log($"[{name}] Reset to baselineTick=0"); + } + + protected virtual void OnDisable() => Reset(); + protected virtual void OnEnable() => Reset(); + + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // OnSerialize(initial) is called every time when a player starts observing us. + // note this is _not_ called just once on spawn. + + // sync target component's position on spawn. + // fixes https://github.com/vis2k/Mirror/pull/3051/ + // (Spawn message wouldn't sync NTChild positions either) + if (initialState) + { + // spawn message is used as first baseline. + TransformSnapshot snapshot = ConstructSnapshot(); + + // always include the tick for deltas to compare against. + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + writer.WriteByte(frameCount); + + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) writer.WriteQuaternion(snapshot.rotation); + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + lastSerializedBaselinePosition = snapshot.position; + lastSerializedBaselineRotation = snapshot.rotation; + } + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // sync target component's position on spawn. + // fixes https://github.com/vis2k/Mirror/pull/3051/ + // (Spawn message wouldn't sync NTChild positions either) + if (initialState) + { + // save last deserialized baseline tick number to compare deltas against + lastDeserializedBaselineTick = reader.ReadByte(); + Vector3 position = Vector3.zero; + Quaternion rotation = Quaternion.identity; + + if (syncPosition) + { + position = reader.ReadVector3(); + lastDeserializedBaselinePosition = position; + } + if (syncRotation) + { + rotation = reader.ReadQuaternion(); + lastDeserializedBaselineRotation = rotation; + } + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnServerToClientDeltaSync(lastDeserializedBaselineTick, position, rotation);//, scale); + } + } + // CUSTOM CHANGE /////////////////////////////////////////////////////////// + // Don't run OnGUI or draw gizmos in debug builds. + // OnGUI allocates even if it does nothing. avoid in release. + //#if UNITY_EDITOR || DEVELOPMENT_BUILD +#if UNITY_EDITOR + // debug /////////////////////////////////////////////////////////////// + // END CUSTOM CHANGE /////////////////////////////////////////////////////// + protected virtual void OnGUI() + { + if (!showOverlay) return; + + // show data next to player for easier debugging. this is very useful! + // IMPORTANT: this is basically an ESP hack for shooter games. + // DO NOT make this available with a hotkey in release builds + if (!Debug.isDebugBuild) return; + + // project position to screen + Vector3 point = Camera.main.WorldToScreenPoint(target.position); + + // enough alpha, in front of camera and in screen? + if (point.z >= 0 && Utils.IsPointInScreen(point)) + { + GUI.color = overlayColor; + GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100)); + + // always show both client & server buffers so it's super + // obvious if we accidentally populate both. + GUILayout.Label($"Server Buffer:{serverSnapshots.Count}"); + GUILayout.Label($"Client Buffer:{clientSnapshots.Count}"); + + GUILayout.EndArea(); + GUI.color = Color.white; + } + } + + protected virtual void DrawGizmos(SortedList buffer) + { + // only draw if we have at least two entries + if (buffer.Count < 2) return; + + // calculate threshold for 'old enough' snapshots + double threshold = NetworkTime.localTime - NetworkClient.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 + TransformSnapshot entry = buffer.Values[i]; + bool oldEnough = entry.localTime <= 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, target.position); + Gizmos.color = Color.white; + Gizmos.DrawLine(target.position, buffer.Values[1].position); + } + + protected virtual void OnDrawGizmos() + { + // This fires in edit mode but that spams NRE's so check isPlaying + if (!Application.isPlaying) return; + if (!showGizmos) return; + + if (isServer) DrawGizmos(serverSnapshots); + if (isClient) DrawGizmos(clientSnapshots); + } +#endif + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs.meta new file mode 100644 index 000000000..10cf28cb4 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f63ea2e505fd484193fb31c5c55ca73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From f9cea8340fddacf1ec032035fa07d9ada3b5ec28 Mon Sep 17 00:00:00 2001 From: miwarnec Date: Wed, 30 Oct 2024 12:13:23 +0100 Subject: [PATCH 02/14] fix mrg grid issue; fix unreliable sending just because baseline changed --- .../NetworkTransformHybrid2022.cs | 250 ++++++++++-------- 1 file changed, 136 insertions(+), 114 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index b583bf617..2a915c51e 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -68,6 +68,20 @@ public class NetworkTransformHybrid2022 : NetworkBehaviour [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] public bool onlySyncOnChange = true; + // change detection: we need to do this carefully in order to get it right. + // + // DONT just check changes in UpdateBaseline(). this would introduce MrG's grid issue: + // server start in A1, reliable baseline sent to client + // server moves to A2, unreliabe delta sent to client + // server moves to A1, nothing is sent to client becuase last baseline position == position + // => client wouldn't know we moved back to A1 + // + // INSTEAD: every update() check for changes since baseline: + // UpdateDelta() keeps sending only if changed since _baseline_ + // UpdateBaseline() resends if there was any change in the period since last baseline. + // => this avoids the A1->A2->A1 grid issue above + bool changedSinceBaseline = false; + // sensitivity is for changed-detection, // this is != precision, which is for quantization and delta compression. [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] @@ -460,9 +474,11 @@ protected virtual void OnServerToClientDeltaSync(byte baselineTick, Vector3 posi } // update server /////////////////////////////////////////////////////// - bool baselineDirty = true; void UpdateServerBaseline(double localTime) { + // only sync on change: only resend baseline if changed since last. + if (onlySyncOnChange && !changedSinceBaseline) return; + // send a reliable baseline every 1 Hz if (localTime >= lastBaselineTime + baselineInterval) { @@ -472,59 +488,50 @@ void UpdateServerBaseline(double localTime) // TransformSnapshot snapshot = ConstructSnapshot(); target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); - // only send a new reliable baseline if changed since last time - // check if changed (unless that feature is disabled). - // baseline is guaranteed to be delivered over reliable. - // here is the only place where we can check for changes. - if (!onlySyncOnChange || Changed(position, rotation)) //snapshot)) + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + if (syncPosition && syncRotation) { - // reliable just changed. keep sending deltas until it's unchanged again. - baselineDirty = true; - - // save bandwidth by only transmitting what is needed. - // -> ArraySegment with random data is slower since byte[] copying - // -> Vector3? and Quaternion? nullables takes more bandwidth - byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! - if (syncPosition && syncRotation) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - RpcServerToClientBaseline_PositionRotation(frameCount, position, rotation); - } - else if (syncPosition) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - RpcServerToClientBaseline_Position(frameCount, position); - } - else if (syncRotation) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - RpcServerToClientBaseline_Rotation(frameCount, rotation); - } - - // save the last baseline's tick number. - // included in baseline to identify which one it was on client - // included in deltas to ensure they are on top of the correct baseline - lastSerializedBaselineTick = frameCount; - lastBaselineTime = NetworkTime.localTime; - lastSerializedBaselinePosition = position; - lastSerializedBaselineRotation = rotation; - - // perf. & bandwidth optimization: - // send a delta right after baseline to avoid potential head of - // line blocking, or skip the delta whenever we sent reliable? - // for example: - // 1 Hz baseline - // 10 Hz delta - // => 11 Hz total if we still send delta after reliable - // => 10 Hz total if we skip delta after reliable - // in that case, skip next delta by simply resetting last delta sync's time. - if (baselineIsDelta) lastDeltaTime = localTime; + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + RpcServerToClientBaseline_PositionRotation(frameCount, position, rotation); } - // indicate that we should stop sending deltas now - else baselineDirty = false; + else if (syncPosition) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + RpcServerToClientBaseline_Position(frameCount, position); + } + else if (syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + RpcServerToClientBaseline_Rotation(frameCount, rotation); + } + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + lastSerializedBaselinePosition = position; + lastSerializedBaselineRotation = rotation; + + // baseline was just sent after a change. reset change detection. + changedSinceBaseline = false; + + // perf. & bandwidth optimization: + // send a delta right after baseline to avoid potential head of + // line blocking, or skip the delta whenever we sent reliable? + // for example: + // 1 Hz baseline + // 10 Hz delta + // => 11 Hz total if we still send delta after reliable + // => 10 Hz total if we skip delta after reliable + // in that case, skip next delta by simply resetting last delta sync's time. + if (baselineIsDelta) lastDeltaTime = localTime; } } @@ -561,17 +568,27 @@ void UpdateServerDelta(double localTime) // here by checking IsClientWithAuthority. // TODO send same time that NetworkServer sends time snapshot? - // only sync on change: - // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. - // if baseline is dirty, send unreliables every sendInterval until baseline is not dirty anymore. - if (onlySyncOnChange && !baselineDirty) return; - if (localTime >= lastDeltaTime + sendInterval) // CUSTOM CHANGE: allow custom sendRate + sendInterval again { // perf: get position/rotation directly. TransformSnapshot is too expensive. // TransformSnapshot snapshot = ConstructSnapshot(); target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + // look for changes every unreliable sendInterval! + // every reliable interval isn't enough, this would cause MrG's grid issue: + // server start in A1, reliable baseline sent to client + // server moves to A2, unreliabe delta sent to client + // server moves to A1, nothing is sent to client becuase last baseline position == position + // => client wouldn't know we moved back to A1 + // every update works, but it's unnecessary overhead since sends only happen every sendInterval + // every unreliable sendInterval is the perfect place to look for changes. + if (onlySyncOnChange && Changed(position, rotation)) + changedSinceBaseline = true; + + // only sync on change: + // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. + if (onlySyncOnChange && !changedSinceBaseline) return; + // save bandwidth by only transmitting what is needed. // -> ArraySegment with random data is slower since byte[] copying // -> Vector3? and Quaternion? nullables takes more bandwidth @@ -662,6 +679,9 @@ void UpdateServer() // update client /////////////////////////////////////////////////////// void UpdateClientBaseline(double localTime) { + // only sync on change: only resend baseline if changed since last. + if (onlySyncOnChange && !changedSinceBaseline) return; + // send a reliable baseline every 1 Hz if (localTime >= lastBaselineTime + baselineInterval) { @@ -669,69 +689,55 @@ void UpdateClientBaseline(double localTime) // TransformSnapshot snapshot = ConstructSnapshot(); target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); - // only send a new reliable baseline if changed since last time - // check if changed (unless that feature is disabled). - // baseline is guaranteed to be delivered over reliable. - // here is the only place where we can check for changes. - if (!onlySyncOnChange || Changed(position, rotation)) //snapshot)) + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + if (syncPosition && syncRotation) { - // reliable just changed. keep sending deltas until it's unchanged again. - baselineDirty = true; - - // save bandwidth by only transmitting what is needed. - // -> ArraySegment with random data is slower since byte[] copying - // -> Vector3? and Quaternion? nullables takes more bandwidth - byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! - if (syncPosition && syncRotation) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - CmdClientToServerBaseline_PositionRotation(frameCount, position, rotation); - } - else if (syncPosition) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - CmdClientToServerBaseline_Position(frameCount, position); - } - else if (syncRotation) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - CmdClientToServerBaseline_Rotation(frameCount, rotation); - } - - // save the last baseline's tick number. - // included in baseline to identify which one it was on client - // included in deltas to ensure they are on top of the correct baseline - lastSerializedBaselineTick = frameCount; - lastBaselineTime = NetworkTime.localTime; - lastSerializedBaselinePosition = position; - lastSerializedBaselineRotation = rotation; - - // perf. & bandwidth optimization: - // send a delta right after baseline to avoid potential head of - // line blocking, or skip the delta whenever we sent reliable? - // for example: - // 1 Hz baseline - // 10 Hz delta - // => 11 Hz total if we still send delta after reliable - // => 10 Hz total if we skip delta after reliable - // in that case, skip next delta by simply resetting last delta sync's time. - if (baselineIsDelta) lastDeltaTime = localTime; + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + CmdClientToServerBaseline_PositionRotation(frameCount, position, rotation); } - // indicate that we should stop sending deltas now - else baselineDirty = false; + else if (syncPosition) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + CmdClientToServerBaseline_Position(frameCount, position); + } + else if (syncRotation) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + CmdClientToServerBaseline_Rotation(frameCount, rotation); + } + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + lastSerializedBaselinePosition = position; + lastSerializedBaselineRotation = rotation; + + // baseline was just sent after a change. reset change detection. + changedSinceBaseline = false; + + // perf. & bandwidth optimization: + // send a delta right after baseline to avoid potential head of + // line blocking, or skip the delta whenever we sent reliable? + // for example: + // 1 Hz baseline + // 10 Hz delta + // => 11 Hz total if we still send delta after reliable + // => 10 Hz total if we skip delta after reliable + // in that case, skip next delta by simply resetting last delta sync's time. + if (baselineIsDelta) lastDeltaTime = localTime; } } void UpdateClientDelta(double localTime) { - // only sync on change: - // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. - // if baseline is dirty, send unreliables every sendInterval until baseline is not dirty anymore. - if (onlySyncOnChange && !baselineDirty) return; - // send to server each 'sendInterval' // NetworkTime.localTime for double precision until Unity has it too // @@ -758,6 +764,22 @@ void UpdateClientDelta(double localTime) // TransformSnapshot snapshot = ConstructSnapshot(); target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + // look for changes every unreliable sendInterval! + // + // every reliable interval isn't enough, this would cause MrG's grid issue: + // client start in A1, reliable baseline sent to server + // client moves to A2, unreliabe delta sent to server + // client moves to A1, nothing is sent to server becuase last baseline position == position + // => server wouldn't know we moved back to A1 + // every update works, but it's unnecessary overhead since sends only happen every sendInterval + // every unreliable sendInterval is the perfect place to look for changes. + if (onlySyncOnChange && Changed(position, rotation)) + changedSinceBaseline = true; + + // only sync on change: + // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. + if (onlySyncOnChange && !changedSinceBaseline) return; + // save bandwidth by only transmitting what is needed. // -> ArraySegment with random data is slower since byte[] copying // -> Vector3? and Quaternion? nullables takes more bandwidth @@ -976,7 +998,7 @@ public virtual void Reset() lastSerializedBaselineTick = 0; lastSerializedBaselinePosition = Vector3.zero; lastSerializedBaselineRotation = Quaternion.identity; - baselineDirty = true; + changedSinceBaseline = false; lastDeserializedBaselineTick = 0; lastDeserializedBaselinePosition = Vector3.zero; From 86629ef95a5f04402ccd55d1a8b5d67c5213c55e Mon Sep 17 00:00:00 2001 From: miwarnec Date: Fri, 1 Nov 2024 11:35:13 +0100 Subject: [PATCH 03/14] comment onserialize baseline --- .../NetworkTransform/NetworkTransformHybrid2022.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index 2a915c51e..2b3188e27 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -1030,9 +1030,16 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) if (syncPosition) writer.WriteVector3(snapshot.position); if (syncRotation) writer.WriteQuaternion(snapshot.rotation); - // save the last baseline's tick number. - // included in baseline to identify which one it was on client - // included in deltas to ensure they are on top of the correct baseline + // IMPORTANT + // OnSerialize(initial) is called for the spawn payload whenever + // someone starts observing this object. we always must make + // this the new baseline, otherwise this happens: + // - server broadcasts baseline @ t=1 + // - server broadcasts delta for baseline @ t=1 + // - ... time passes ... + // - new observer -> OnSerialize sends current position @ t=2 + // - server broadcasts delta for baseline @ t=1 + // => client's baseline is t=2 but receives delta for t=1 _!_ lastSerializedBaselineTick = frameCount; lastBaselineTime = NetworkTime.localTime; lastSerializedBaselinePosition = snapshot.position; From 9400136f0affb111c08725e1c9944bb6e5c4354a Mon Sep 17 00:00:00 2001 From: mischa <16416509+miwarnec@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:38:29 +0100 Subject: [PATCH 04/14] Update Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs Co-authored-by: MrGadget <9826063+MrGadget1024@users.noreply.github.com> --- .../NetworkTransform/NetworkTransformHybrid2022.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index 2b3188e27..105f81692 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -22,12 +22,6 @@ public class NetworkTransformHybrid2022 : NetworkBehaviour [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] public Transform target; - // TODO SyncDirection { ClientToServer, ServerToClient } is easier? - [Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")] - [Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually. - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - public bool clientAuthority; - // Is this a client with authority over this transform? // This component could be on the player object or any object that has been assigned authority to this client. protected bool IsClientWithAuthority => isClient && authority; From f55be4be1e344303404ca1acf16cfb7eaa813884 Mon Sep 17 00:00:00 2001 From: mischa <16416509+miwarnec@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:38:36 +0100 Subject: [PATCH 05/14] Update Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs Co-authored-by: MrGadget <9826063+MrGadget1024@users.noreply.github.com> --- .../NetworkTransformHybrid2022.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index 105f81692..10d2bd3af 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -127,21 +127,6 @@ protected override void OnValidate() // use sendRate instead of syncInterval for now syncInterval = 0; - - // cache squared precisions - // positionPrecisionSqr = positionPrecision * positionPrecision; - // scalePrecisionSqr = scalePrecision * scalePrecision; - - // obsolete clientAuthority compatibility: - // if it was used, then set the new SyncDirection automatically. - // if it wasn't used, then don't touch syncDirection. - #pragma warning disable CS0618 - if (clientAuthority) - { - syncDirection = SyncDirection.ClientToServer; - Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead."); - } - #pragma warning restore CS0618 } // snapshot functions ////////////////////////////////////////////////// From 595dd9da2448f5261b1094ee10e0287aa5b99556 Mon Sep 17 00:00:00 2001 From: mischa <16416509+miwarnec@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:38:57 +0100 Subject: [PATCH 06/14] Update Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs Co-authored-by: MrGadget <9826063+MrGadget1024@users.noreply.github.com> --- .../Components/NetworkTransform/NetworkTransformHybrid2022.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index 10d2bd3af..87ee6228e 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -19,7 +19,7 @@ public class NetworkTransformHybrid2022 : NetworkBehaviour { // target transform to sync. can be on a child. [Header("Target")] - [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] + [Tooltip("The Transform component to sync. May be on this GameObject, or on a child.")] public Transform target; // Is this a client with authority over this transform? From 0985f6d04aa8b6ff666e3f13f0f594e6b0d54243 Mon Sep 17 00:00:00 2001 From: mischa <16416509+miwarnec@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:39:06 +0100 Subject: [PATCH 07/14] Update Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs Co-authored-by: MrGadget <9826063+MrGadget1024@users.noreply.github.com> --- .../Components/NetworkTransform/NetworkTransformHybrid2022.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index 87ee6228e..f90c8b52d 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -39,7 +39,7 @@ public class NetworkTransformHybrid2022 : NetworkBehaviour public float sendInterval => 1f / sendRate; // END CUSTOM CHANGE - [Tooltip("Ocassionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] + [Tooltip("Occasionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] public int baselineRate = 1; public float baselineInterval => baselineRate < int.MaxValue ? 1f / baselineRate : 0; // for 1 Hz, that's 1000ms double lastBaselineTime; From 5f841da317dfcb1669510964f75a66b4b15f886c Mon Sep 17 00:00:00 2001 From: mischa <16416509+miwarnec@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:39:16 +0100 Subject: [PATCH 08/14] Update Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs Co-authored-by: MrGadget <9826063+MrGadget1024@users.noreply.github.com> --- .../Components/NetworkTransform/NetworkTransformHybrid2022.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index f90c8b52d..92118d49d 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -83,7 +83,7 @@ public class NetworkTransformHybrid2022 : NetworkBehaviour public float rotationSensitivity = 0.01f; // public float scaleSensitivity = 0.01f; - [Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast paced games since it doubles bandwidth costs.")] + [Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast-paced games since it doubles bandwidth costs.")] public bool unreliableRedundancy = false; [Tooltip("When sending a reliable baseline, should we also send an unreliable delta or rely on the reliable baseline to arrive in a similar time?")] From 2bf6428eafe700861b03660269b0a6c96b265335 Mon Sep 17 00:00:00 2001 From: mischa <16416509+miwarnec@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:39:38 +0100 Subject: [PATCH 09/14] Update Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs Co-authored-by: MrGadget <9826063+MrGadget1024@users.noreply.github.com> --- .../Components/NetworkTransform/NetworkTransformHybrid2022.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index 92118d49d..ab35196fa 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -278,7 +278,7 @@ protected virtual void OnClientToServerDeltaSync(byte baselineTick, Vector3? pos // only apply if in client authority mode if (syncDirection != SyncDirection.ClientToServer) return; - // protect against ever growing buffer size attacks + // protect against ever-growing buffer size attacks if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; // only player owned objects (with a connection) can send to From 707bb87fb67bbc6d053cfa9ed71e0764d71fec85 Mon Sep 17 00:00:00 2001 From: miwarnec Date: Mon, 4 Nov 2024 10:35:15 +0100 Subject: [PATCH 10/14] nthybrid: debug draw data points --- .../NetworkTransformHybrid2022.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index ab35196fa..abcb37a8f 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -102,6 +102,7 @@ public class NetworkTransformHybrid2022 : NetworkBehaviour // debugging /////////////////////////////////////////////////////////// [Header("Debug")] + public bool debugDraw; public bool showGizmos; public bool showOverlay; public Color overlayColor = new Color(0, 0, 0, 0.5f); @@ -213,6 +214,9 @@ void CmdClientToServerBaseline_PositionRotation(byte baselineTick, Vector3 posit lastDeserializedBaselinePosition = position; lastDeserializedBaselineRotation = rotation; + // debug draw: baseline + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.yellow, 10f); + // if baseline counts as delta, insert it into snapshot buffer too if (baselineIsDelta) OnClientToServerDeltaSync(baselineTick, position, rotation);//, scale); @@ -224,6 +228,9 @@ void CmdClientToServerBaseline_Position(byte baselineTick, Vector3 position) lastDeserializedBaselineTick = baselineTick; lastDeserializedBaselinePosition = position; + // debug draw: baseline + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.yellow, 10f); + // if baseline counts as delta, insert it into snapshot buffer too if (baselineIsDelta) OnClientToServerDeltaSync(baselineTick, position, Quaternion.identity);//, scale); @@ -244,6 +251,9 @@ void CmdClientToServerBaseline_Rotation(byte baselineTick, Quaternion rotation) [Command(channel = Channels.Unreliable)] // unreliable delta void CmdClientToServerDelta_Position(byte baselineTick, Vector3 position) { + // debug draw: delta + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.white, 10f); + // Debug.Log($"[{name}] server received delta for baseline #{lastDeserializedBaselineTick}"); OnClientToServerDeltaSync(baselineTick, position, Quaternion.identity);//, scale); } @@ -258,6 +268,9 @@ void CmdClientToServerDelta_Rotation(byte baselineTick, Quaternion rotation) [Command(channel = Channels.Unreliable)] // unreliable delta void CmdClientToServerDelta_PositionRotation(byte baselineTick, Vector3 position, Quaternion rotation) { + // debug draw: delta + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.white, 10f); + // Debug.Log($"[{name}] server received delta for baseline #{lastDeserializedBaselineTick}"); OnClientToServerDeltaSync(baselineTick, position, rotation);//, scale); } @@ -324,6 +337,9 @@ void RpcServerToClientBaseline_PositionRotation(byte baselineTick, Vector3 posit lastDeserializedBaselinePosition = position; lastDeserializedBaselineRotation = rotation; + // debug draw: baseline + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.yellow, 10f); + // if baseline counts as delta, insert it into snapshot buffer too if (baselineIsDelta) OnServerToClientDeltaSync(baselineTick, position, rotation);//, Vector3.zero);//, scale); @@ -340,6 +356,9 @@ void RpcServerToClientBaseline_Position(byte baselineTick, Vector3 position) lastDeserializedBaselineTick = baselineTick; lastDeserializedBaselinePosition = position; + // debug draw: baseline + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.yellow, 10f); + // if baseline counts as delta, insert it into snapshot buffer too if (baselineIsDelta) OnServerToClientDeltaSync(baselineTick, position, Quaternion.identity);//, Vector3.zero);//, scale); @@ -369,6 +388,9 @@ void RpcServerToClientDelta_PositionRotation(byte baselineTick, Vector3 position // ignore if this object is owned by this client. if (IsClientWithAuthority) return; + // debug draw: delta + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.white, 10f); + OnServerToClientDeltaSync(baselineTick, position, rotation);//, scale); } @@ -380,6 +402,9 @@ void RpcServerToClientDelta_Position(byte baselineTick, Vector3 position) // ignore if this object is owned by this client. if (IsClientWithAuthority) return; + // debug draw: delta + if (debugDraw) Debug.DrawLine(position, position + Vector3.up, Color.white, 10f); + OnServerToClientDeltaSync(baselineTick, position, Quaternion.identity);//, scale); } From c8c632ce6cec5210a967540937947151897fc60c Mon Sep 17 00:00:00 2001 From: miwarnec Date: Mon, 4 Nov 2024 10:41:48 +0100 Subject: [PATCH 11/14] debug draw: drops --- .../NetworkTransform/NetworkTransformHybrid2022.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index abcb37a8f..c6142e368 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -282,6 +282,12 @@ protected virtual void OnClientToServerDeltaSync(byte baselineTick, Vector3? pos // we should never apply a delta on top of a wrong baseline. if (baselineTick != lastDeserializedBaselineTick) { + // debug draw: drop + if (debugDraw) + { + if (position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.red, 10f); + } + // this can happen if unreliable arrives before reliable etc. // no need to log this except when debugging. // Debug.Log($"[{name}] Server: received delta for wrong baseline #{baselineTick} from: {connectionToClient}. Last was {lastDeserializedBaselineTick}. Ignoring."); @@ -426,6 +432,12 @@ protected virtual void OnServerToClientDeltaSync(byte baselineTick, Vector3 posi // we should never apply a delta on top of a wrong baseline. if (baselineTick != lastDeserializedBaselineTick) { + // debug draw: drop + if (debugDraw) + { + Debug.DrawLine(position, position + Vector3.up, Color.red, 10f); + } + // this can happen if unreliable arrives before reliable etc. // no need to log this except when debugging. // Debug.Log($"[{name}] Client: received delta for wrong baseline #{baselineTick}. Last was {lastDeserializedBaselineTick}. Ignoring."); From 93cc81707afb0400309affcc5187f5e9bf264ad3 Mon Sep 17 00:00:00 2001 From: miwarnec Date: Mon, 4 Nov 2024 11:13:26 +0100 Subject: [PATCH 12/14] nthybrid: OnServerToClient checks for host mode first to avoid noise! --- .../NetworkTransformHybrid2022.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index c6142e368..a930c543a 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -428,6 +428,17 @@ void RpcServerToClientDelta_Rotation(byte baselineTick, Quaternion rotation) // server broadcasts sync message to all clients protected virtual void OnServerToClientDeltaSync(byte baselineTick, 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; + // ensure this delta is for our last known baseline. // we should never apply a delta on top of a wrong baseline. if (baselineTick != lastDeserializedBaselineTick) @@ -444,17 +455,6 @@ protected virtual void OnServerToClientDeltaSync(byte baselineTick, Vector3 posi return; } - // 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; - // Debug.Log($"[{name}] Client: received delta for baseline #{baselineTick}"); // on the client, we receive rpcs for all entities. From 6ece0c7d712dc3ed237e4b3e3460fcc170439808 Mon Sep 17 00:00:00 2001 From: miwarnec Date: Mon, 4 Nov 2024 11:25:42 +0100 Subject: [PATCH 13/14] nthybrid: OnClientToServer check ordering --- .../NetworkTransform/NetworkTransformHybrid2022.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index a930c543a..ad9498fd0 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -278,6 +278,9 @@ void CmdClientToServerDelta_PositionRotation(byte baselineTick, Vector3 position // local authority client sends sync message to server for broadcasting protected virtual void OnClientToServerDeltaSync(byte baselineTick, Vector3? position, Quaternion? rotation)//, Vector3? scale) { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + // ensure this delta is for our last known baseline. // we should never apply a delta on top of a wrong baseline. if (baselineTick != lastDeserializedBaselineTick) @@ -294,9 +297,6 @@ protected virtual void OnClientToServerDeltaSync(byte baselineTick, Vector3? pos return; } - // only apply if in client authority mode - if (syncDirection != SyncDirection.ClientToServer) return; - // protect against ever-growing buffer size attacks if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; From 6d6d31b2029db5b28fe306d2ea7d9251f9814576 Mon Sep 17 00:00:00 2001 From: miwarnec Date: Mon, 4 Nov 2024 11:59:38 +0100 Subject: [PATCH 14/14] fix: server doesn't overwrite client authority sync points --- .../NetworkTransform/NetworkTransformHybrid2022.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs index ad9498fd0..309c384b6 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid2022.cs @@ -684,8 +684,13 @@ void UpdateServer() // should we broadcast at all? if (!disableSendingThisToClients) // CUSTOM CHANGE: see comment at definition { - UpdateServerBaseline(localTime); - UpdateServerDelta(localTime); + // only broadcast for server owned objects. + // otherwise server would overwrite ClientToServer object's baselines. + if (syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority) + { + UpdateServerBaseline(localTime); + UpdateServerDelta(localTime); + } } // interpolate remote clients