diff --git a/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs index 0be09c095..ee5154e3c 100644 --- a/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs +++ b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs @@ -1,4 +1,5 @@ // NetworkTransform V3 (reliable) by mischa (2022-10) +using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; @@ -8,12 +9,22 @@ namespace Mirror [AddComponentMenu("Network/Network Transform (Reliable)")] public class NetworkTransformReliable : NetworkTransformBase { + uint sendIntervalCounter = 0; + double lastSendIntervalTime = double.MinValue; + + float onlySyncOnChangeInterval => onlySyncOnChangeCorrectionMultiplier * sendIntervalMultiplier; + [Header("Sync Only If Changed")] [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] public bool onlySyncOnChange = true; [Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")] public float onlySyncOnChangeCorrectionMultiplier = 2; + // uint so non negative. + [Header("Send Interval Multiplier")] + [Tooltip("Send every multiple of Network Manager send interval (= 1 / NM Send Rate).")] + public uint sendIntervalMultiplier = 3; + [Header("Rotation")] [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] public float rotationSensitivity = 0.01f; @@ -43,7 +54,7 @@ public class NetworkTransformReliable : NetworkTransformBase // Used to store last sent snapshots protected TransformSnapshot last; - int lastClientCount = 0; + protected int lastClientCount = 1; // update ////////////////////////////////////////////////////////////// void Update() @@ -55,7 +66,35 @@ void Update() else if (isClient) UpdateClient(); } - void UpdateServer() + void LateUpdate() + { + // set dirty to trigger OnSerialize. either always, or only if changed. + // It has to be checked in LateUpdate() for onlySyncOnChange to avoid + // the possibility of Update() running first before the object's movement + // script's Update(), which then causes NT to send every alternate frame + // instead. + if (isServer || (IsClientWithAuthority && NetworkClient.ready)) // is NetworkClient.ready even needed? + { + if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct()))) + SetDirty(); + + CheckLastSendTime(); + } + } + + protected override void OnTeleport(Vector3 destination) + { + if (isOwned && TryGetComponent(out CharacterController cc)) + { + cc.enabled = false; + base.OnTeleport(destination); + cc.enabled = true; + } + else + base.OnTeleport(destination); + } + + protected virtual void UpdateServer() { // apply buffered snapshots IF client authority // -> in server authority, server moves the object @@ -65,9 +104,7 @@ void UpdateServer() // then we don't need to do anything. // -> connectionToClient is briefly null after scene changes: // https://github.com/MirrorNetworking/Mirror/issues/3329 - if (syncDirection == SyncDirection.ClientToServer && - connectionToClient != null && - !isOwned) + if (syncDirection == SyncDirection.ClientToServer && connectionToClient != null && !isOwned) { if (serverSnapshots.Count > 0) { @@ -82,42 +119,20 @@ void UpdateServer() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); } } - - // set dirty to trigger OnSerialize. either always, or only if changed. - // technically snapshot interpolation requires constant sending. - // however, with reliable it should be fine without constant sends. - // - // detect changes _after_ all changes were applied above. - if (!onlySyncOnChange || Changed(Construct())) - SetDirty(); } - void UpdateClient() + protected virtual void UpdateClient() { // client authority, and local player (= allowed to move myself)? - if (IsClientWithAuthority) + if (!IsClientWithAuthority) { - // https://github.com/vis2k/Mirror/pull/2992/ - if (!NetworkClient.ready) return; - - // set dirty to trigger OnSerialize. either always, or only if changed. - // technically snapshot interpolation requires constant sending. - // however, with reliable it should be fine without constant sends. - if (!onlySyncOnChange || Changed(Construct())) - SetDirty(); - } - // for all other clients (and for local player if !authority), - // we need to apply snapshots from the buffer - else - { - // only while we have snapshots if (clientSnapshots.Count > 0) { - // step the interpolation without touching time. // NetworkClient is responsible for time globally. SnapshotInterpolation.StepInterpolation( @@ -129,8 +144,8 @@ void UpdateClient() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - Apply(computed, to); + Apply(computed, to); } // 'only sync if moved' @@ -147,6 +162,26 @@ void UpdateClient() } } + protected virtual void CheckLastSendTime() + { + // timeAsDouble not available in older Unity versions. +#if !UNITY_2020_3_OR_NEWER + if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime)) + { + if (sendIntervalCounter == sendIntervalMultiplier) + sendIntervalCounter = 0; + sendIntervalCounter++; + } +#else + if (AccurateInterval.Elapsed(Time.timeAsDouble, NetworkServer.sendInterval, ref lastSendIntervalTime)) + { + if (sendIntervalCounter == sendIntervalMultiplier) + sendIntervalCounter = 0; + sendIntervalCounter++; + } + } +#endif + // check if position / rotation / scale changed since last sync protected virtual bool Changed(TransformSnapshot current) => // position is quantized and delta compressed. @@ -191,7 +226,21 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) // initial if (initialState) { - if (syncPosition) writer.WriteVector3(snapshot.position); + // If there is a last serialized snapshot, we use it. + // This prevents the new client getting a snapshot that is different + // from what the older clients last got. If this happens, and on the next + // regular serialisation the delta compression will get wrong values. + // Notes: + // 1. Interestingly only the older clients have it wrong, because at the end + // of the function, last = snapshot which is the initial state's snapshot + // 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate + // snapshot constructed would have been the same as the last anyway. + if (last.remoteTime > 0) + snapshot = last; + + if (syncPosition) + writer.WriteVector3(snapshot.position); + if (syncRotation) { // (optional) smallest three compression for now. no delta. @@ -200,7 +249,9 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) else writer.WriteQuaternion(snapshot.rotation); } - if (syncScale) writer.WriteVector3(snapshot.scale); + + if (syncScale) + writer.WriteVector3(snapshot.scale); } // delta else @@ -213,6 +264,7 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized); DeltaCompression.Compress(writer, lastSerializedPosition, quantized); } + if (syncRotation) { // (optional) smallest three compression for now. no delta. @@ -221,20 +273,20 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) else writer.WriteQuaternion(snapshot.rotation); } + if (syncScale) { // quantize -> delta -> varint Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized); DeltaCompression.Compress(writer, lastSerializedScale, quantized); } - - // int written = writer.Position - before; - // Debug.Log($"{name} compressed to {written} bytes"); } // save serialized as 'last' for next delta compression - if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); - if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); + if (syncPosition) + Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); + if (syncScale) + Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); // set 'last' last = snapshot; @@ -249,7 +301,9 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) // initial if (initialState) { - if (syncPosition) position = reader.ReadVector3(); + if (syncPosition) + position = reader.ReadVector3(); + if (syncRotation) { // (optional) smallest three compression for now. no delta. @@ -258,7 +312,9 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) else rotation = reader.ReadQuaternion(); } - if (syncScale) scale = reader.ReadVector3(); + + if (syncScale) + scale = reader.ReadVector3(); } // delta else @@ -269,6 +325,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition); position = Compression.ScaleToFloat(quantized, positionPrecision); } + if (syncRotation) { // (optional) smallest three compression for now. no delta. @@ -277,6 +334,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) else rotation = reader.ReadQuaternion(); } + if (syncScale) { Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale); @@ -286,12 +344,16 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) // handle depending on server / client / host. // server has priority for host mode. - if (isServer) OnClientToServerSync(position, rotation, scale); - else if (isClient) OnServerToClientSync(position, rotation, scale); + if (isServer) + OnClientToServerSync(position, rotation, scale); + else if (isClient) + OnServerToClientSync(position, rotation, scale); // save deserialized as 'last' for next delta compression - if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); - if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); + if (syncPosition) + Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); + if (syncScale) + Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); } // sync //////////////////////////////////////////////////////////////// @@ -306,21 +368,19 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; // 'only sync on change' needs a correction on every new move sequence. - if (onlySyncOnChange && - NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval, onlySyncOnChangeCorrectionMultiplier)) + if (onlySyncOnChange && NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval, onlySyncOnChangeInterval)) { RewriteHistory( serverSnapshots, connectionToClient.remoteTimeStamp, - NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline. - NetworkServer.sendInterval, // Unity 2019 doesn't have timeAsDouble yet + NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline. + NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet target.localPosition, target.localRotation, target.localScale); - // Debug.Log($"{name}: corrected history on server to fix initial stutter after not sending for a while."); } - AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp, position, rotation, scale); + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + NetworkServer.sendInterval * sendIntervalMultiplier, position, rotation, scale); } // server broadcasts sync message to all clients @@ -331,20 +391,19 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat // 'only sync on change' needs a correction on every new move sequence. if (onlySyncOnChange && - NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval, onlySyncOnChangeCorrectionMultiplier)) + NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeInterval)) { RewriteHistory( clientSnapshots, - NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline. - NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet - NetworkClient.sendInterval, + NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + NetworkClient.sendInterval * sendIntervalMultiplier, target.localPosition, target.localRotation, target.localScale); - // Debug.Log($"{name}: corrected history on client to fix initial stutter after not sending for a while."); } - AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp, position, rotation, scale); + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + NetworkClient.sendInterval * sendIntervalMultiplier, position, rotation, scale); } // only sync on change ///////////////////////////////////////////////// @@ -359,9 +418,12 @@ static bool NeedsCorrection( SortedList snapshots, double remoteTimestamp, double bufferTime, - double toleranceMultiplier) => - snapshots.Count == 1 && - remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier; + double toleranceMultiplier) + { + bool value = snapshots.Count == 1 && + remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier; + return value; + } // 2. insert a fake snapshot at current position, // exactly one 'sendInterval' behind the newly received one. @@ -380,6 +442,7 @@ static void RewriteHistory( // insert a fake one at where we used to be, // 'sendInterval' behind the new one. + SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot( remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time. localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet @@ -399,6 +462,8 @@ public override void Reset() lastSerializedScale = Vector3Long.zero; lastDeserializedScale = Vector3Long.zero; + + last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero); } } }