From f4d76ca49f2a2819b56006e2a2daeb7924409bee Mon Sep 17 00:00:00 2001 From: mischa Date: Thu, 5 Sep 2024 12:21:21 +0200 Subject: [PATCH] quake, single commit --- .../NetworkRigidbodyUnreliableCompressed.cs | 115 +++++ ...tworkRigidbodyUnreliableCompressed.cs.meta | 11 + .../NetworkTransformReliable.cs | 10 + .../NetworkTransformUnreliable.cs | 14 +- .../NetworkTransformUnreliableCompressed.cs | 480 ++++++++++++++++++ ...tworkTransformUnreliableCompressed.cs.meta | 11 + .../Mirror/Examples/Tanks/Prefabs/Tank.prefab | 61 +-- Assets/Mirror/Examples/Tanks/Scripts/Tank.cs | 26 +- 8 files changed, 679 insertions(+), 49 deletions(-) create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs new file mode 100644 index 000000000..e29f3c9d3 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody (Unreliable Compressed)")] + public class NetworkRigidbodyUnreliableCompressed : NetworkTransformUnreliableCompressed + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody rb; + bool wasKinematic; + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // we can't overwrite .target to be a Rigidbody. + // but we can ensure that .target has a Rigidbody, and use it. + if (target.GetComponent() == null) + { + Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + } + } + + // cach Rigidbody and original isKinematic setting + protected override void Awake() + { + // we can't overwrite .target to be a Rigidbody. + // but we can use its Rigidbody component. + rb = target.GetComponent(); + if (rb == null) + { + Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + return; + } + wasKinematic = rb.isKinematic; + base.Awake(); + } + + // reset forced isKinematic flag to original. + // otherwise the overwritten value would remain between sessions forever. + // for example, a game may run as client, set rigidbody.iskinematic=true, + // then run as server, where .iskinematic isn't touched and remains at + // the overwritten=true, even though the user set it to false originally. + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; + + // overwriting Construct() and Apply() to set Rigidbody.MovePosition + // would give more jittery movement. + + // FixedUpdate for physics + void FixedUpdate() + { + // who ever has authority moves the Rigidbody with physics. + // everyone else simply sets it to kinematic. + // so that only the Transform component is synced. + + // host mode + if (isServer && isClient) + { + // in host mode, we own it it if: + // clientAuthority is disabled (hence server / we own it) + // clientAuthority is enabled and we have authority over this object. + bool owned = !clientAuthority || IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // client only + else if (isClient) + { + // on the client, we own it only if clientAuthority is enabled, + // and we have authority over this object. + bool owned = IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // server only + else if (isServer) + { + // on the server, we always own it if clientAuthority is disabled. + bool owned = !clientAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta new file mode 100644 index 000000000..c1fb167c3 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f830b261ed7644a4b1cc262cf36fc96 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs index b81433773..78e63a8f5 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -42,6 +42,16 @@ public class NetworkTransformReliable : NetworkTransformBase // Used to store last sent snapshots protected TransformSnapshot last; + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to reliable + syncMethod = SyncMethod.Reliable; + } + // update ////////////////////////////////////////////////////////////// void Update() { diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs index 235a65a1d..3d16bb72f 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -30,6 +30,16 @@ public class NetworkTransformUnreliable : NetworkTransformBase protected Changed cachedChangedComparison; protected bool hasSentUnchangedPosition; + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to unreliable + syncMethod = SyncMethod.Unreliable; + } + // update ////////////////////////////////////////////////////////////// // Update applies interpolation void Update() @@ -316,7 +326,7 @@ protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnaps } if (syncRotation) - { + { if (compressRotation) { bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; @@ -459,7 +469,7 @@ protected virtual void UpdateSyncData(ref SyncData syncData, SortedList receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale) { using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload)) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs new file mode 100644 index 000000000..f3473749c --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs @@ -0,0 +1,480 @@ +// NetworkTransform V3 based on NetworkTransformUnreliable, using Mirror's new +// Unreliable quake style networking model with delta compression. +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Unreliable Compressed)")] + public class NetworkTransformUnreliableCompressed : NetworkTransformBase + { + [Header("Additional Settings")] + [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; + + [Header("Rotation")] + [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] + public float rotationSensitivity = 0.01f; + + // delta compression is capable of detecting byte-level changes. + // if we scale float position to bytes, + // then small movements will only change one byte. + // this gives optimal bandwidth. + // benchmark with 0.01 precision: 130 KB/s => 60 KB/s + // benchmark with 0.1 precision: 130 KB/s => 30 KB/s + [Header("Precision")] + [Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")] + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float positionPrecision = 0.01f; // 1 cm + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float rotationPrecision = 0.001f; // this is for the quaternion's components, needs to be small + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float scalePrecision = 0.01f; // 1 cm + + [Header("Debug")] + public bool debugDraw = false; + + // 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. + protected Vector3Long lastSerializedPosition = Vector3Long.zero; + protected Vector3Long lastDeserializedPosition = Vector3Long.zero; + + protected Vector4Long lastSerializedRotation = Vector4Long.zero; + protected Vector4Long lastDeserializedRotation = Vector4Long.zero; + + protected Vector3Long lastSerializedScale = Vector3Long.zero; + protected Vector3Long lastDeserializedScale = Vector3Long.zero; + + // Used to store last sent snapshots + protected TransformSnapshot last; + + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to unreliable + syncMethod = SyncMethod.Unreliable; + + // Unreliable ignores syncInterval. don't need to force anymore: + // sendIntervalMultiplier = 1; + } + + // update ////////////////////////////////////////////////////////////// + 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(); + } + + 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)) + { + if (!onlySyncOnChange || Changed(Construct())) + SetDirty(); + } + } + + protected virtual void UpdateServer() + { + // 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. + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + } + } + } + + protected virtual void UpdateClient() + { + // client authority, and local player (= allowed to move myself)? + if (!IsClientWithAuthority) + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + + if (debugDraw) + { + Debug.DrawLine(from.position, to.position, Color.white, 10f); + Debug.DrawLine(computed.position, computed.position + Vector3.up, Color.white, 10f); + } + } + } + } + + // check if position / rotation / scale changed since last _full reliable_ sync. + protected virtual bool Changed(TransformSnapshot current) => + // position is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.position, current.position, positionPrecision) || + // rotation isn't quantized / delta compressed. + // check with sensitivity. + Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity || + // scale is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.scale, current.scale, scalePrecision); + + // helper function to compare quantized representations of a Vector3 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision) + { + Compression.ScaleToLong(u, precision, out Vector3Long uQuantized); + Compression.ScaleToLong(v, precision, out Vector3Long vQuantized); + return uQuantized != vQuantized; + } + + // Unreliable OnSerialize: + // - initial=true sends reliable full state + // - initial=false sends unreliable delta states + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // get current snapshot for broadcasting. + TransformSnapshot snapshot = Construct(); + + // ClientToServer optimization: + // for interpolated client owned identities, + // always broadcast the latest known snapshot so other clients can + // interpolate immediately instead of catching up too + + // TODO dirty mask? [compression is very good w/o it already] + // each vector's component is delta compressed. + // an unchanged component would still require 1 byte. + // let's use a dirty bit mask to filter those out as well. + + // Debug.Log($"NT OnSerialize: initial={initialState} method={syncMethod}"); + + // reliable full state + if (initialState) + { + // TODO initialState is now sent multiple times. find a new fix for this: + // 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 this 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; + + int startPosition = writer.Position; + + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // if smallest-three quaternion compression is enabled, + // then we don't need baseline rotation since delta always + // sends an absolute value. + if (!compressRotation) + { + writer.WriteQuaternion(snapshot.rotation); + } + } + if (syncScale) writer.WriteVector3(snapshot.scale); + + // save serialized as 'last' for next delta compression. + // only for reliable full sync, since unreliable isn't guaranteed to arrive. + if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); + if (syncRotation && !compressRotation) Compression.ScaleToLong(snapshot.rotation, rotationPrecision, out lastSerializedRotation); + if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); + + // set 'last' + last = snapshot; + } + // unreliable delta: compress against last full reliable state + else + { + int startPosition = writer.Position; + + if (syncPosition) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedPosition, quantized); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + { + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + } + else + { + // quantize -> delta -> varint + // this works for quaternions too, where xyzw are [-1,1] + // and gradually change as rotation changes. + Compression.ScaleToLong(snapshot.rotation, rotationPrecision, out Vector4Long quantized); + DeltaCompression.Compress(writer, lastSerializedRotation, quantized); + } + } + if (syncScale) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedScale, quantized); + } + } + } + + // Unreliable OnDeserialize: + // - initial=true sends reliable full state + // - initial=false sends unreliable delta states + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + // reliable full state + if (initialState) + { + if (syncPosition) + { + position = reader.ReadVector3(); + + if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.green, 10.0f); + } + if (syncRotation) + { + // if smallest-three quaternion compression is enabled, + // then we don't need baseline rotation since delta always + // sends an absolute value. + if (!compressRotation) + { + rotation = reader.ReadQuaternion(); + } + } + if (syncScale) scale = reader.ReadVector3(); + + // save deserialized as 'last' for next delta compression. + // only for reliable full sync, since unreliable isn't guaranteed to arrive. + if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); + if (syncRotation && !compressRotation) Compression.ScaleToLong(rotation.Value, rotationPrecision, out lastDeserializedRotation); + if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); + } + // unreliable delta: decompress against last full reliable state + else + { + // varint -> delta -> quantize + if (syncPosition) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition); + position = Compression.ScaleToFloat(quantized, positionPrecision); + + if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.yellow, 10.0f); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + { + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + } + else + { + // varint -> delta -> quantize + // this works for quaternions too, where xyzw are [-1,1] + // and gradually change as rotation changes. + Vector4Long quantized = DeltaCompression.Decompress(reader, lastDeserializedRotation); + rotation = Compression.ScaleToFloat(quantized, rotationPrecision); + } + } + if (syncScale) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale); + scale = Compression.ScaleToFloat(quantized, scalePrecision); + } + + // handle depending on server / client / host. + // server has priority for host mode. + // + // only do this for the unreliable delta states! + // processing the reliable baselines shows noticeable jitter + // around baseline syncs (e.g. tanks demo @ 4 Hz sendRate). + // unreliable deltas are always within the same time delta, + // so this gives perfectly smooth results. + if (isServer) OnClientToServerSync(position, rotation, scale); + else if (isClient) OnServerToClientSync(position, rotation, scale); + } + } + + // sync //////////////////////////////////////////////////////////////// + + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + 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 * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + serverSnapshots, + connectionToClient.remoteTimeStamp, + NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline. + NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet + GetPosition(), + GetRotation(), + GetScale()); + } + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + clientSnapshots, + NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + NetworkClient.sendInterval * sendIntervalMultiplier, + GetPosition(), + GetRotation(), + GetScale()); + } + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); + } + + // only sync on change ///////////////////////////////////////////////// + // snap interp. needs a continous flow of packets. + // 'only sync on change' interrupts it while not changed. + // once it restarts, snap interp. will interp from the last old position. + // this will cause very noticeable stutter for the first move each time. + // the fix is quite simple. + + // 1. detect if the remaining snapshot is too old from a past move. + static bool NeedsCorrection( + SortedList snapshots, + double remoteTimestamp, + double bufferTime, + double toleranceMultiplier) => + snapshots.Count == 1 && + remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier; + + // 2. insert a fake snapshot at current position, + // exactly one 'sendInterval' behind the newly received one. + static void RewriteHistory( + SortedList snapshots, + // timestamp of packet arrival, not interpolated remote time! + double remoteTimeStamp, + double localTime, + double sendInterval, + Vector3 position, + Quaternion rotation, + Vector3 scale) + { + // clear the previous snapshot + snapshots.Clear(); + + // insert a fake one at where we used to be, + // 'sendInterval' behind the new one. + SnapshotInterpolation.InsertIfNotExists( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + new TransformSnapshot( + remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time. + localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet + position, + rotation, + scale + ) + ); + } + + // reset state for next session. + // do not ever call this during a session (i.e. after teleport). + // calling this will break delta compression. + public override void ResetState() + { + base.ResetState(); + + // reset delta + lastSerializedPosition = Vector3Long.zero; + lastDeserializedPosition = Vector3Long.zero; + + lastSerializedRotation = Vector4Long.zero; + lastDeserializedRotation = Vector4Long.zero; + + lastSerializedScale = Vector3Long.zero; + lastDeserializedScale = Vector3Long.zero; + + // reset 'last' for delta too + last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero); + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta new file mode 100644 index 000000000..5eef915ca --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d993bac37a92145448c1ea027b3e9ddc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab b/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab index 7da88c445..77e155a31 100644 --- a/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab +++ b/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab @@ -11,7 +11,6 @@ GameObject: - component: {fileID: 4492442352427800} - component: {fileID: 114118589361100106} - component: {fileID: 114250499875391520} - - component: {fileID: 3464953498043699706} - component: {fileID: 114654712548978148} - component: {fileID: 6900008319038825817} m_Layer: 0 @@ -31,6 +30,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5803173220413450940} - {fileID: 2155495746218491392} @@ -50,7 +50,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 - _assetId: 3454335836 + _assetId: 2638947628 serverOnly: 0 visibility: 0 hasSpawned: 0 @@ -63,9 +63,10 @@ MonoBehaviour: m_GameObject: {fileID: 1916082411674582} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3} + m_Script: {fileID: 11500000, guid: d993bac37a92145448c1ea027b3e9ddc, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 1 syncDirection: 1 syncMode: 0 syncInterval: 0 @@ -79,47 +80,14 @@ MonoBehaviour: interpolateRotation: 1 interpolateScale: 0 coordinateSpace: 0 - timelineOffset: 0 + timelineOffset: 1 showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - bufferResetMultiplier: 3 - positionSensitivity: 0.01 + onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 - scaleSensitivity: 0.01 ---- !u!114 &3464953498043699706 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1916082411674582} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3} - m_Name: - m_EditorClassIdentifier: - syncDirection: 1 - syncMode: 0 - syncInterval: 0 - target: {fileID: 5803173220413450936} - syncPosition: 0 - syncRotation: 1 - syncScale: 0 - onlySyncOnChange: 1 - compressRotation: 1 - interpolatePosition: 0 - interpolateRotation: 1 - interpolateScale: 0 - coordinateSpace: 0 - timelineOffset: 0 - showGizmos: 0 - showOverlay: 0 - overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - bufferResetMultiplier: 3 - positionSensitivity: 0.01 - rotationSensitivity: 0.01 - scaleSensitivity: 0.01 + positionPrecision: 0.01 + scalePrecision: 0.01 --- !u!114 &114654712548978148 MonoBehaviour: m_ObjectHideFlags: 0 @@ -132,6 +100,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 7deadf756194d461e9140e42d651693b, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 1 syncDirection: 0 syncMode: 0 syncInterval: 0.1 @@ -196,6 +165,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 5, z: 0} m_LocalScale: {x: 0.1, y: 0.1, z: 0.1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 4492442352427800} m_RootOrder: 1 @@ -211,10 +181,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -239,6 +211,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!102 &955977906578811009 TextMesh: serializedVersion: 3 @@ -337,9 +310,9 @@ PrefabInstance: objectReference: {fileID: 0} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} ---- !u!4 &5803173220413450940 stripped +--- !u!4 &606281948174800110 stripped Transform: - m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} m_PrefabInstance: {fileID: 7130959241934869977} m_PrefabAsset: {fileID: 0} @@ -355,9 +328,9 @@ Transform: type: 3} m_PrefabInstance: {fileID: 7130959241934869977} m_PrefabAsset: {fileID: 0} ---- !u!4 &606281948174800110 stripped +--- !u!4 &5803173220413450940 stripped Transform: - m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c, + m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} m_PrefabInstance: {fileID: 7130959241934869977} m_PrefabAsset: {fileID: 0} diff --git a/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs b/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs index 28550bfcb..99b20832f 100644 --- a/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs +++ b/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs @@ -20,16 +20,24 @@ public class Tank : NetworkBehaviour public Transform projectileMount; [Header("Stats")] - [SyncVar] public int health = 5; + public int health = 5; + int lastHealth = 5; void Update() { + // manual setdirty test + if (health != lastHealth) + { + SetDirty(); + lastHealth = health; + } + // always update health bar. // (SyncVar hook would only update on clients, not on server) healthBar.text = new string('-', health); - + // take input from focused window only - if(!Application.isFocused) return; + if(!Application.isFocused) return; // movement for local player if (isLocalPlayer) @@ -91,5 +99,17 @@ void RotateTurret() turret.transform.LookAt(lookRotation); } } + + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // Debug.LogWarning($"Tank {name} OnSerialize {(initialState ? "full" : "delta")} health={health}"); + writer.WriteInt(health); + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + health = reader.ReadInt(); + // Debug.LogWarning($"Tank {name} OnDeserialize {(initialState ? "full" : "delta")} health={health}"); + } } }