From 82a96a28f03d49d7f780975dc6c5e5c547c197fc Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:24:27 -0500 Subject: [PATCH 01/22] style(Attributes): removed space --- Assets/Mirror/Core/Attributes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Mirror/Core/Attributes.cs b/Assets/Mirror/Core/Attributes.cs index 55fa36bb4..df8236a8f 100644 --- a/Assets/Mirror/Core/Attributes.cs +++ b/Assets/Mirror/Core/Attributes.cs @@ -87,5 +87,5 @@ public class ShowInInspectorAttribute : Attribute {} /// Used to make a field readonly in the inspector /// [AttributeUsage(AttributeTargets.Field)] - public class ReadOnlyAttribute : PropertyAttribute { } + public class ReadOnlyAttribute : PropertyAttribute {} } From 4e5e7a18c41cfd96f7daa05acae703f6cb598833 Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:27:44 -0500 Subject: [PATCH 02/22] fix(NetworkAnimator): Added gameObject to Debug.LogWarnings --- Assets/Mirror/Components/NetworkAnimator.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Assets/Mirror/Components/NetworkAnimator.cs b/Assets/Mirror/Components/NetworkAnimator.cs index ce0781efc..7510ff599 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs +++ b/Assets/Mirror/Components/NetworkAnimator.cs @@ -352,7 +352,7 @@ void ReadParameters(NetworkReader reader) byte parameterCount = reader.ReadByte(); if (parameterCount != parameters.Length) { - Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?"); + Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject); return; } @@ -423,7 +423,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) byte layerCount = reader.ReadByte(); if (layerCount != animator.layerCount) { - Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?"); + Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject); return; } @@ -461,13 +461,13 @@ public void SetTrigger(int hash) { if (!isClient) { - Debug.LogWarning("Tried to set animation in the server for a client-controlled animator"); + Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject); return; } if (!isOwned) { - Debug.LogWarning("Only the client with authority can set animations"); + Debug.LogWarning("Only the client with authority can set animations", gameObject); return; } @@ -481,7 +481,7 @@ public void SetTrigger(int hash) { if (!isServer) { - Debug.LogWarning("Tried to set animation in the client for a server-controlled animator"); + Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject); return; } @@ -508,13 +508,13 @@ public void ResetTrigger(int hash) { if (!isClient) { - Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator"); + Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject); return; } if (!isOwned) { - Debug.LogWarning("Only the client with authority can reset animations"); + Debug.LogWarning("Only the client with authority can reset animations", gameObject); return; } @@ -528,7 +528,7 @@ public void ResetTrigger(int hash) { if (!isServer) { - Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator"); + Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject); return; } From ea136ee265061f4b3b30d2451da68efb57532654 Mon Sep 17 00:00:00 2001 From: mischa Date: Sat, 13 Jan 2024 21:33:47 +0100 Subject: [PATCH 03/22] Prediction: recordInterval option --- .../Components/PredictedRigidbody/PredictedRigidbody.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 7a4e197c4..7c05c9427 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -35,6 +35,7 @@ public class PredictedRigidbody : NetworkBehaviour [Header("State History")] public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough readonly SortedList stateHistory = new SortedList(); + public float recordInterval = 0.050f; [Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")] public bool compareLastFirst = true; @@ -224,8 +225,14 @@ void FixedUpdate() // manually store last recorded so we can easily check against this // without traversing the SortedList. RigidbodyState lastRecorded; + double lastRecordTime; void RecordState() { + // instead of recording every fixedupdate, let's record in an interval. + // we don't want to record every tiny move and correct too hard. + if (NetworkTime.time < lastRecordTime + recordInterval) return; + lastRecordTime = NetworkTime.time; + // NetworkTime.time is always behind by bufferTime. // prediction aims to be on the exact same server time (immediately). // use predictedTime to record state, otherwise we would record in the past. From d3f916fc25c2428be52ddb9d4444cc5452460b96 Mon Sep 17 00:00:00 2001 From: mischa Date: Sun, 14 Jan 2024 11:18:40 +0100 Subject: [PATCH 04/22] Prediction: inverse control flow where instead of separating renderers, we separate physics --- .../PredictedRigidbody/PredictedRigidbody.cs | 290 ++++++++++++------ .../PredictedRigidbodyPhysics.cs | 72 +++++ ...meta => PredictedRigidbodyPhysics.cs.meta} | 0 .../PredictedRigidbodyVisual.cs | 64 ---- 4 files changed, 263 insertions(+), 163 deletions(-) create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs rename Assets/Mirror/Components/PredictedRigidbody/{PredictedRigidbodyVisual.cs.meta => PredictedRigidbodyPhysics.cs.meta} (100%) delete mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyVisual.cs diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 7c05c9427..86b853447 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -20,10 +20,10 @@ public enum CorrectionMode Move, // rigidbody.MovePosition/Rotation } - [RequireComponent(typeof(Rigidbody))] + // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. public class PredictedRigidbody : NetworkBehaviour { - Rigidbody rb; + Rigidbody rb; // own rigidbody on server. this is never moved to a physics copy. Vector3 lastPosition; // [Tooltip("Broadcast changes if position changed by more than ... meters.")] @@ -61,7 +61,6 @@ public class PredictedRigidbody : NetworkBehaviour public bool showGhost = true; public float ghostDistanceThreshold = 0.1f; public float ghostEnabledCheckInterval = 0.2f; - double lastGhostEnabledCheckTime = 0; [Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")] public Material ghostMaterial; @@ -76,41 +75,97 @@ public class PredictedRigidbody : NetworkBehaviour [Header("Debugging")] public float lineTime = 10; - // visually interpolated GameObject copy for smoothing - protected GameObject visualCopy; - protected MeshRenderer[] originalRenderers; + // Rigidbody & Collider are moved out into a separate object. + // this way the visual object can smoothly follow. + protected GameObject physicsCopy; + Rigidbody physicsCopyRigidbody; // caching to avoid GetComponent + Collider physicsCopyCollider; // caching to avoid GetComponent void Awake() { rb = GetComponent(); } - // instantiate a visually-only copy of the gameobject to apply smoothing. - // on clients, where players are watching. - // create & destroy methods are virtual so games with a different - // rendering setup / hierarchy can inject their own copying code here. - protected virtual void CreateVisualCopy() + protected virtual Rigidbody MoveRigidbody(GameObject destination) { - // create an empty GameObject with the same name + _Visual - visualCopy = new GameObject($"{name}_Visual"); - visualCopy.transform.position = transform.position; - visualCopy.transform.rotation = transform.rotation; - visualCopy.transform.localScale = transform.localScale; + Rigidbody source = GetComponent(); + Rigidbody rigidbodyCopy = destination.AddComponent(); + rigidbodyCopy.mass = source.mass; + rigidbodyCopy.drag = source.drag; + rigidbodyCopy.angularDrag = source.angularDrag; + rigidbodyCopy.useGravity = source.useGravity; + rigidbodyCopy.isKinematic = source.isKinematic; + rigidbodyCopy.interpolation = source.interpolation; + rigidbodyCopy.collisionDetectionMode = source.collisionDetectionMode; + rigidbodyCopy.constraints = source.constraints; + rigidbodyCopy.sleepThreshold = source.sleepThreshold; + rigidbodyCopy.freezeRotation = source.freezeRotation; + rigidbodyCopy.position = source.position; + rigidbodyCopy.rotation = source.rotation; + rigidbodyCopy.velocity = source.velocity; + Destroy(source); + return rigidbodyCopy; + } - // add the PredictedRigidbodyVisual component - PredictedRigidbodyVisual visualRigidbody = visualCopy.AddComponent(); - visualRigidbody.target = this; - visualRigidbody.positionInterpolationSpeed = positionInterpolationSpeed; - visualRigidbody.rotationInterpolationSpeed = rotationInterpolationSpeed; - visualRigidbody.teleportDistanceMultiplier = teleportDistanceMultiplier; + protected virtual void MoveBoxColliders(GameObject destination) + { + BoxCollider[] sourceColliders = GetComponents(); + foreach (BoxCollider sourceCollider in sourceColliders) + { + BoxCollider colliderCopy = destination.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.size = sourceCollider.size; + Destroy(sourceCollider); + } + } - // copy the rendering components + protected virtual void MoveSphereColliders(GameObject destination) + { + SphereCollider[] sourceColliders = GetComponents(); + foreach (SphereCollider sourceCollider in sourceColliders) + { + SphereCollider colliderCopy = destination.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + Destroy(sourceCollider); + } + } + + protected virtual void MoveCapsuleColliders(GameObject destination) + { + CapsuleCollider[] sourceColliders = GetComponents(); + foreach (CapsuleCollider sourceCollider in sourceColliders) + { + CapsuleCollider colliderCopy = destination.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.height = sourceCollider.height; + colliderCopy.direction = sourceCollider.direction; + Destroy(sourceCollider); + } + } + + protected virtual void MoveMeshColliders(GameObject destination) + { + MeshCollider[] sourceColliders = GetComponents(); + foreach (MeshCollider sourceCollider in sourceColliders) + { + MeshCollider colliderCopy = destination.AddComponent(); + colliderCopy.sharedMesh = sourceCollider.sharedMesh; + colliderCopy.convex = sourceCollider.convex; + colliderCopy.isTrigger = sourceCollider.isTrigger; + Destroy(sourceCollider); + } + } + + protected virtual void CopyRenderersAsGhost(GameObject destination) + { if (TryGetComponent(out MeshRenderer originalMeshRenderer)) { - MeshFilter meshFilter = visualCopy.AddComponent(); + MeshFilter meshFilter = destination.AddComponent(); meshFilter.mesh = GetComponent().mesh; - MeshRenderer meshRenderer = visualCopy.AddComponent(); + MeshRenderer meshRenderer = destination.AddComponent(); meshRenderer.material = originalMeshRenderer.material; // renderers often have multiple materials. copy all. @@ -119,74 +174,108 @@ protected virtual void CreateVisualCopy() Material[] materials = new Material[originalMeshRenderer.materials.Length]; for (int i = 0; i < materials.Length; ++i) { - materials[i] = originalMeshRenderer.materials[i]; + materials[i] = ghostMaterial; } meshRenderer.materials = materials; // need to reassign to see it in effect } } // if we didn't find a renderer, show a warning else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy()."); + } - // find renderers in children. - // this will be used a lot later, so only find them once when - // creating the visual copy here. - originalRenderers = GetComponentsInChildren(); + // instantiate a physics-only copy of the gameobject to apply corrections. + // this way the main visual object can smoothly follow. + // it's best to separate the physics instead of separating the renderers. + // some projects have complex rendering / animation setups which we can't touch. + // besides, Rigidbody+Collider are two components, where as renders may be many. + protected virtual void SeparatePhysics() + { + // skip if already separated + if (physicsCopy != null) return; - // replace this renderer's materials with the ghost (if enabled) - foreach (MeshRenderer rend in originalRenderers) + Debug.Log($"Separating Physics for {name}"); + + // create an empty GameObject with the same name + _Physical + physicsCopy = new GameObject($"{name}_Physical"); + physicsCopy.transform.position = transform.position; + physicsCopy.transform.rotation = transform.rotation; + physicsCopy.transform.localScale = transform.localScale; + + // add the PredictedRigidbodyPhysical component + PredictedRigidbodyPhysics physicsRigidbody = physicsCopy.AddComponent(); + physicsRigidbody.target = this; + + // move the rigidbody component to the physics GameObject + MoveRigidbody(physicsCopy); + + // move the collider components to the physics GameObject + MoveBoxColliders(physicsCopy); + MoveSphereColliders(physicsCopy); + MoveCapsuleColliders(physicsCopy); + MoveMeshColliders(physicsCopy); + + // show ghost by copying all renderers / materials with ghost material applied + if (showGhost) { - if (showGhost) - { - // renderers often have multiple materials. replace all. - if (rend.materials != null) - { - Material[] materials = rend.materials; - for (int i = 0; i < materials.Length; ++i) - { - materials[i] = ghostMaterial; - } - rend.materials = materials; // need to reassign to see it in effect - } - } - else - { - rend.enabled = false; - } + CopyRenderersAsGhost(physicsCopy); + physicsRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; + physicsRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; } + + // cache components to avoid GetComponent calls at runtime + physicsCopyRigidbody = physicsCopy.GetComponent(); + physicsCopyCollider = physicsCopy.GetComponent(); + if (physicsRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); + if (physicsCopyCollider == null) throw new Exception("SeparatePhysics: couldn't find final Collider."); } - protected virtual void DestroyVisualCopy() + protected virtual void DestroyPhysicsCopy() { - if (visualCopy != null) Destroy(visualCopy); + if (physicsCopy != null) Destroy(physicsCopy); } - protected virtual void UpdateVisualCopy() + protected virtual void SmoothFollowPhysicsCopy() { - // only if visual copy was already created - if (visualCopy == null || originalRenderers == null) return; + // hard follow: + // transform.position = physicsCopyCollider.position; + // transform.rotation = physicsCopyCollider.rotation; - // enough to run this in a certain interval. - // doing this every update would be overkill. - // this is only for debug purposes anyway. - if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return; - lastGhostEnabledCheckTime = NetworkTime.localTime; + // if we are further than N colliders sizes behind, then teleport + float colliderSize = physicsCopyCollider.bounds.size.magnitude; + float threshold = colliderSize * teleportDistanceMultiplier; + float distance = Vector3.Distance(transform.position, physicsCopyRigidbody.position); + if (distance > threshold) + { + transform.position = physicsCopyRigidbody.position; + transform.rotation = physicsCopyRigidbody.rotation; + Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}"); + return; + } - // only show ghost while interpolating towards the object. - // if we are 'inside' the object then don't show ghost. - // otherwise it just looks like z-fighting the whole time. - // => iterated the renderers we found when creating the visual copy. - // we don't want to GetComponentsInChildren every time here! - bool insideTarget = Vector3.Distance(transform.position, visualCopy.transform.position) <= ghostDistanceThreshold; - foreach (MeshRenderer rend in originalRenderers) - rend.enabled = !insideTarget; + // smoothly interpolate to the target position. + // speed relative to how far away we are + float positionStep = distance * positionInterpolationSpeed; + // speed relative to how far away we are. + // => speed increases by distance² because the further away, the + // sooner we need to catch the fuck up + // float positionStep = (distance * distance) * interpolationSpeed; + transform.position = Vector3.MoveTowards(transform.position, physicsCopyRigidbody.position, positionStep * Time.deltaTime); + + // smoothly interpolate to the target rotation. + // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. + transform.rotation = Quaternion.Slerp(transform.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime); } // creater visual copy only on clients, where players are watching. - public override void OnStartClient() => CreateVisualCopy(); + public override void OnStartClient() + { + // OnDeserialize may have already created this + if (physicsCopy == null) SeparatePhysics(); + } // destroy visual copy only in OnStopClient(). // OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed. - public override void OnStopClient() => DestroyVisualCopy(); + public override void OnStopClient() => DestroyPhysicsCopy(); void UpdateServer() { @@ -204,15 +293,14 @@ void UpdateServer() SetDirty(); } - void UpdateClient() - { - UpdateVisualCopy(); - } - void Update() { if (isServer) UpdateServer(); - if (isClient) UpdateClient(); + } + + void LateUpdate() + { + if (isClient) SmoothFollowPhysicsCopy(); } void FixedUpdate() @@ -257,20 +345,20 @@ void RecordState() if (stateHistory.Count > 0) { RigidbodyState last = stateHistory.Values[stateHistory.Count - 1]; - positionDelta = rb.position - last.position; - velocityDelta = rb.velocity - last.velocity; - rotationDelta = rb.rotation * Quaternion.Inverse(last.rotation); // this is how you calculate a quaternion delta + positionDelta = physicsCopyRigidbody.position - last.position; + velocityDelta = physicsCopyRigidbody.velocity - last.velocity; + rotationDelta = physicsCopyRigidbody.rotation * Quaternion.Inverse(last.rotation); // this is how you calculate a quaternion delta // debug draw the recorded state - Debug.DrawLine(last.position, rb.position, Color.red, lineTime); + Debug.DrawLine(last.position, physicsCopyRigidbody.position, Color.red, lineTime); } // create state to insert RigidbodyState state = new RigidbodyState( predictedTime, - positionDelta, rb.position, - rotationDelta, rb.rotation, - velocityDelta, rb.velocity + positionDelta, physicsCopyRigidbody.position, + rotationDelta, physicsCopyRigidbody.rotation, + velocityDelta, physicsCopyRigidbody.velocity ); // add state to history @@ -289,13 +377,13 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) // fix rigidbodies seemingly dancing in place instead of coming to rest. // hard snap to the position below a threshold velocity. // this is fine because the visual object still smoothly interpolates to it. - if (rb.velocity.magnitude <= snapThreshold) + if (physicsCopyRigidbody.velocity.magnitude <= snapThreshold) { - Debug.Log($"Prediction: snapped {name} into place because velocity {rb.velocity.magnitude:F3} <= {snapThreshold:F3}"); + Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); stateHistory.Clear(); - rb.position = position; - rb.rotation = rotation; - rb.velocity = Vector3.zero; + physicsCopyRigidbody.position = position; + physicsCopyRigidbody.rotation = rotation; + physicsCopyRigidbody.velocity = Vector3.zero; // user callback OnSnappedIntoPlace(); @@ -306,17 +394,17 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) // TODO is this a good idea? what about next capture while it's interpolating? if (correctionMode == CorrectionMode.Move) { - rb.MovePosition(position); - rb.MoveRotation(rotation); + physicsCopyRigidbody.MovePosition(position); + physicsCopyRigidbody.MoveRotation(rotation); } else if (correctionMode == CorrectionMode.Set) { - rb.position = position; - rb.rotation = rotation; + physicsCopyRigidbody.position = position; + physicsCopyRigidbody.rotation = rotation; } // there's only one way to set velocity - rb.velocity = velocity; + physicsCopyRigidbody.velocity = velocity; } // process a received server state. @@ -342,8 +430,8 @@ void OnReceivedState(double timestamp, RigidbodyState state) // // if this ever causes issues, feel free to disable it. if (compareLastFirst && - Vector3.Distance(state.position, rb.position) < positionCorrectionThreshold && - Quaternion.Angle(state.rotation, rb.rotation) < rotationCorrectionThreshold) + Vector3.Distance(state.position, physicsCopyRigidbody.position) < positionCorrectionThreshold && + Quaternion.Angle(state.rotation, physicsCopyRigidbody.rotation) < rotationCorrectionThreshold) { // Debug.Log($"OnReceivedState for {name}: taking optimized early return!"); return; @@ -388,7 +476,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // we clamp it to 'now'. // but only correct if off by threshold. // TODO maybe we should interpolate this back to 'now'? - if (Vector3.Distance(state.position, rb.position) >= positionCorrectionThreshold) + if (Vector3.Distance(state.position, physicsCopyRigidbody.position) >= positionCorrectionThreshold) { double ahead = state.timestamp - newest.timestamp; Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); @@ -435,7 +523,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // for example, on same machine with near zero latency. // int correctedAmount = stateHistory.Count - afterIndex; // Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); - Debug.DrawLine(rb.position, recomputed.position, Color.green, lineTime); + Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); ApplyState(recomputed.position, recomputed.rotation, recomputed.velocity); // user callback @@ -458,14 +546,18 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) // sending server's current deltaTime is the safest option. // client then applies it on top of remoteTimestamp. writer.WriteFloat(Time.deltaTime); - writer.WriteVector3(rb.position); - writer.WriteQuaternion(rb.rotation); - writer.WriteVector3(rb.velocity); + writer.WriteVector3(rb.position); // own rigidbody on server, it's never moved to physics copy + writer.WriteQuaternion(rb.rotation); // own rigidbody on server, it's never moved to physics copy + writer.WriteVector3(rb.velocity); // own rigidbody on server, it's never moved to physics copy } // read the server's state, compare with client state & correct if necessary. public override void OnDeserialize(NetworkReader reader, bool initialState) { + // this may be called before OnStartClient. + // in that case, separate physics first before applying state. + if (physicsCopy == null) SeparatePhysics(); + // deserialize data // we want to know the time on the server when this was sent, which is remoteTimestamp. double timestamp = NetworkClient.connection.remoteTimeStamp; diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs new file mode 100644 index 000000000..1c855a4c6 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs @@ -0,0 +1,72 @@ +// Prediction moves out the Rigidbody & Collider into a separate object. +// This way the main (visual) object can smoothly follow it, instead of hard. +using System; +using UnityEngine; + +namespace Mirror +{ + public class PredictedRigidbodyPhysics : MonoBehaviour + { + [Tooltip("The predicted rigidbody owner.")] + public PredictedRigidbody target; + + // ghost (settings are copyed from PredictedRigidbody) + MeshRenderer ghost; + public float ghostDistanceThreshold = 0.1f; + public float ghostEnabledCheckInterval = 0.2f; + double lastGhostEnabledCheckTime = 0; + + // cache + Collider co; + + // we add this component manually from PredictedRigidbody. + // so assign this in Start. target isn't set in Awake yet. + void Start() + { + co = GetComponent(); + ghost = GetComponent(); + } + + void UpdateGhostRenderers() + { + // only if a ghost renderer was given + if (ghost == null) return; + + // enough to run this in a certain interval. + // doing this every update would be overkill. + // this is only for debug purposes anyway. + if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return; + lastGhostEnabledCheckTime = NetworkTime.localTime; + + // only show ghost while interpolating towards the object. + // if we are 'inside' the object then don't show ghost. + // otherwise it just looks like z-fighting the whole time. + // => iterated the renderers we found when creating the visual copy. + // we don't want to GetComponentsInChildren every time here! + bool insideTarget = Vector3.Distance(transform.position, target.transform.position) <= ghostDistanceThreshold; + ghost.enabled = !insideTarget; + } + + void Update() => UpdateGhostRenderers(); + + // always follow in late update, after update modified positions + void LateUpdate() + { + // if owner gets network destroyed for any reason, destroy visual + if (target == null || target.gameObject == null) Destroy(gameObject); + } + + // also show a yellow gizmo for the predicted & corrected physics. + // in case we can't renderer ghosts, at least we have this. + void OnDrawGizmos() + { + if (co != null) + { + // show the client's predicted & corrected physics in yellow + Bounds bounds = co.bounds; + Gizmos.color = Color.yellow; + Gizmos.DrawWireCube(bounds.center, bounds.size); + } + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyVisual.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs.meta similarity index 100% rename from Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyVisual.cs.meta rename to Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs.meta diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyVisual.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyVisual.cs deleted file mode 100644 index ef875cba1..000000000 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyVisual.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using UnityEngine; - -namespace Mirror -{ - public class PredictedRigidbodyVisual : MonoBehaviour - { - [Tooltip("The predicted rigidbody to follow.")] - public PredictedRigidbody target; - Rigidbody targetRigidbody; - - // settings are applied in the other PredictedRigidbody component and then copied here. - [HideInInspector] public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least - [HideInInspector] public float rotationInterpolationSpeed = 10; - [HideInInspector] public float teleportDistanceMultiplier = 10; - - // we add this component manually from PredictedRigidbody. - // so assign this in Start. target isn't set in Awake yet. - void Start() - { - targetRigidbody = target.GetComponent(); - } - - // always follow in late update, after update modified positions - void LateUpdate() - { - // if target gets network destroyed for any reason, destroy visual - if (targetRigidbody == null || target.gameObject == null) - { - Destroy(gameObject); - return; - } - - // hard follow: - // transform.position = targetRigidbody.position; - // transform.rotation = targetRigidbody.rotation; - - // if we are further than N colliders sizes behind, then teleport - float colliderSize = target.GetComponent().bounds.size.magnitude; - float threshold = colliderSize * teleportDistanceMultiplier; - float distance = Vector3.Distance(transform.position, targetRigidbody.position); - if (distance > threshold) - { - transform.position = targetRigidbody.position; - transform.rotation = targetRigidbody.rotation; - Debug.Log($"[PredictedRigidbodyVisual] Teleported because distance {distance:F2} > threshold {threshold:F2}"); - return; - } - - // smoothly interpolate to the target position. - // speed relative to how far away we are - float positionStep = distance * positionInterpolationSpeed; - // speed relative to how far away we are. - // => speed increases by distance² because the further away, the - // sooner we need to catch the fuck up - // float positionStep = (distance * distance) * interpolationSpeed; - transform.position = Vector3.MoveTowards(transform.position, targetRigidbody.position, positionStep * Time.deltaTime); - - // smoothly interpolate to the target rotation. - // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. - transform.rotation = Quaternion.Slerp(transform.rotation, targetRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime); - } - } -} From 0cb96901b862ee09c6f97d25961ef5c0fa97ea84 Mon Sep 17 00:00:00 2001 From: mischa Date: Sun, 14 Jan 2024 13:09:16 +0100 Subject: [PATCH 05/22] Prediction: add server state ghost too --- ...ostMaterial.mat => LocalGhostMaterial.mat} | 7 +- ...l.mat.meta => LocalGhostMaterial.mat.meta} | 0 .../PredictedRigidbody/PredictedRigidbody.cs | 52 +++++++++--- .../PredictedRigidbody.cs.meta | 5 +- ...s.cs => PredictedRigidbodyPhysicsGhost.cs} | 2 +- ...=> PredictedRigidbodyPhysicsGhost.cs.meta} | 0 .../PredictedRigidbodyRemoteGhost.cs | 53 ++++++++++++ .../PredictedRigidbodyRemoteGhost.cs.meta | 3 + .../RemoteGhostMaterial.mat | 85 +++++++++++++++++++ .../RemoteGhostMaterial.mat.meta | 8 ++ 10 files changed, 197 insertions(+), 18 deletions(-) rename Assets/Mirror/Components/PredictedRigidbody/{GhostMaterial.mat => LocalGhostMaterial.mat} (92%) rename Assets/Mirror/Components/PredictedRigidbody/{GhostMaterial.mat.meta => LocalGhostMaterial.mat.meta} (100%) rename Assets/Mirror/Components/PredictedRigidbody/{PredictedRigidbodyPhysics.cs => PredictedRigidbodyPhysicsGhost.cs} (97%) rename Assets/Mirror/Components/PredictedRigidbody/{PredictedRigidbodyPhysics.cs.meta => PredictedRigidbodyPhysicsGhost.cs.meta} (100%) create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat create mode 100644 Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta diff --git a/Assets/Mirror/Components/PredictedRigidbody/GhostMaterial.mat b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat similarity index 92% rename from Assets/Mirror/Components/PredictedRigidbody/GhostMaterial.mat rename to Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat index 49d28b1fa..ff29a7316 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/GhostMaterial.mat +++ b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat @@ -7,8 +7,10 @@ Material: m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_Name: GhostMaterial + m_Name: LocalGhostMaterial m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 m_ValidKeywords: - _ALPHAPREMULTIPLY_ON m_InvalidKeywords: [] @@ -19,6 +21,7 @@ Material: stringTagMap: RenderType: Transparent disabledShaderPasses: [] + m_LockedProperties: m_SavedProperties: serializedVersion: 3 m_TexEnvs: @@ -77,6 +80,6 @@ Material: - _UVSec: 0 - _ZWrite: 0 m_Colors: - - _Color: {r: 1, g: 1, b: 1, a: 0.11764706} + - _Color: {r: 1, g: 0, b: 0.067070484, a: 0.15686275} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Components/PredictedRigidbody/GhostMaterial.mat.meta b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta similarity index 100% rename from Assets/Mirror/Components/PredictedRigidbody/GhostMaterial.mat.meta rename to Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 86b853447..ccfdeb8a7 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -63,7 +63,8 @@ public class PredictedRigidbody : NetworkBehaviour public float ghostEnabledCheckInterval = 0.2f; [Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")] - public Material ghostMaterial; + public Material localGhostMaterial; + public Material remoteGhostMaterial; [Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")] public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least @@ -81,6 +82,9 @@ public class PredictedRigidbody : NetworkBehaviour Rigidbody physicsCopyRigidbody; // caching to avoid GetComponent Collider physicsCopyCollider; // caching to avoid GetComponent + // we also create one extra ghost for the exact known server state. + protected GameObject remoteCopy; + void Awake() { rb = GetComponent(); @@ -158,7 +162,7 @@ protected virtual void MoveMeshColliders(GameObject destination) } } - protected virtual void CopyRenderersAsGhost(GameObject destination) + protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) { if (TryGetComponent(out MeshRenderer originalMeshRenderer)) { @@ -174,7 +178,7 @@ protected virtual void CopyRenderersAsGhost(GameObject destination) Material[] materials = new Material[originalMeshRenderer.materials.Length]; for (int i = 0; i < materials.Length; ++i) { - materials[i] = ghostMaterial; + materials[i] = material; } meshRenderer.materials = materials; // need to reassign to see it in effect } @@ -188,7 +192,7 @@ protected virtual void CopyRenderersAsGhost(GameObject destination) // it's best to separate the physics instead of separating the renderers. // some projects have complex rendering / animation setups which we can't touch. // besides, Rigidbody+Collider are two components, where as renders may be many. - protected virtual void SeparatePhysics() + protected virtual void CreateGhosts() { // skip if already separated if (physicsCopy != null) return; @@ -202,8 +206,10 @@ protected virtual void SeparatePhysics() physicsCopy.transform.localScale = transform.localScale; // add the PredictedRigidbodyPhysical component - PredictedRigidbodyPhysics physicsRigidbody = physicsCopy.AddComponent(); - physicsRigidbody.target = this; + PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent(); + physicsGhostRigidbody.target = this; + physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; + physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; // move the rigidbody component to the physics GameObject MoveRigidbody(physicsCopy); @@ -217,21 +223,31 @@ protected virtual void SeparatePhysics() // show ghost by copying all renderers / materials with ghost material applied if (showGhost) { - CopyRenderersAsGhost(physicsCopy); - physicsRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; - physicsRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; + // one for the locally predicted rigidbody + CopyRenderersAsGhost(physicsCopy, localGhostMaterial); + physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; + physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; + + // one for the latest remote state for comparison + remoteCopy = new GameObject($"{name}_Remote"); + PredictedRigidbodyRemoteGhost predictedGhost = remoteCopy.AddComponent(); + predictedGhost.target = this; + predictedGhost.ghostDistanceThreshold = ghostDistanceThreshold; + predictedGhost.ghostEnabledCheckInterval = ghostEnabledCheckInterval; + CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial); } // cache components to avoid GetComponent calls at runtime physicsCopyRigidbody = physicsCopy.GetComponent(); physicsCopyCollider = physicsCopy.GetComponent(); - if (physicsRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); + if (physicsGhostRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); if (physicsCopyCollider == null) throw new Exception("SeparatePhysics: couldn't find final Collider."); } - protected virtual void DestroyPhysicsCopy() + protected virtual void DestroyCopies() { if (physicsCopy != null) Destroy(physicsCopy); + if (remoteCopy != null) Destroy(remoteCopy); } protected virtual void SmoothFollowPhysicsCopy() @@ -270,12 +286,12 @@ protected virtual void SmoothFollowPhysicsCopy() public override void OnStartClient() { // OnDeserialize may have already created this - if (physicsCopy == null) SeparatePhysics(); + if (physicsCopy == null) CreateGhosts(); } // destroy visual copy only in OnStopClient(). // OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed. - public override void OnStopClient() => DestroyPhysicsCopy(); + public override void OnStopClient() => DestroyCopies(); void UpdateServer() { @@ -411,6 +427,14 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) // compares it against our history and applies corrections if needed. void OnReceivedState(double timestamp, RigidbodyState state) { + // always update remote state ghost + if (remoteCopy != null) + { + remoteCopy.transform.position = state.position; + remoteCopy.transform.rotation = state.rotation; + remoteCopy.transform.localScale = transform.localScale; + } + // OPTIONAL performance optimization when comparing idle objects. // even idle objects will have a history of ~32 entries. // sampling & traversing through them is unnecessarily costly. @@ -556,7 +580,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) { // this may be called before OnStartClient. // in that case, separate physics first before applying state. - if (physicsCopy == null) SeparatePhysics(); + if (physicsCopy == null) CreateGhosts(); // deserialize data // we want to know the time on the server when this was sent, which is remoteTimestamp. diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta index b128e6ab9..645605858 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta @@ -4,7 +4,10 @@ MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: - - ghostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} + - localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, + type: 2} + - remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, + type: 2} executionOrder: 0 icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs similarity index 97% rename from Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs rename to Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs index 1c855a4c6..5e4674987 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs @@ -5,7 +5,7 @@ namespace Mirror { - public class PredictedRigidbodyPhysics : MonoBehaviour + public class PredictedRigidbodyPhysicsGhost : MonoBehaviour { [Tooltip("The predicted rigidbody owner.")] public PredictedRigidbody target; diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta similarity index 100% rename from Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysics.cs.meta rename to Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs new file mode 100644 index 000000000..24b37b624 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs @@ -0,0 +1,53 @@ +// simply ghost object that always follows last received server state. +using UnityEngine; + +namespace Mirror +{ + public class PredictedRigidbodyRemoteGhost : MonoBehaviour + { + [Tooltip("The predicted rigidbody owner.")] + public PredictedRigidbody target; + + // ghost (settings are copyed from PredictedRigidbody) + MeshRenderer ghost; + public float ghostDistanceThreshold = 0.1f; + public float ghostEnabledCheckInterval = 0.2f; + double lastGhostEnabledCheckTime = 0; + + // we add this component manually from PredictedRigidbody. + // so assign this in Start. target isn't set in Awake yet. + void Start() + { + ghost = GetComponent(); + } + + void UpdateGhostRenderers() + { + // only if a ghost renderer was given + if (ghost == null) return; + + // enough to run this in a certain interval. + // doing this every update would be overkill. + // this is only for debug purposes anyway. + if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return; + lastGhostEnabledCheckTime = NetworkTime.localTime; + + // only show ghost while interpolating towards the object. + // if we are 'inside' the object then don't show ghost. + // otherwise it just looks like z-fighting the whole time. + // => iterated the renderers we found when creating the visual copy. + // we don't want to GetComponentsInChildren every time here! + bool insideTarget = Vector3.Distance(transform.position, target.transform.position) <= ghostDistanceThreshold; + ghost.enabled = !insideTarget; + } + + void Update() => UpdateGhostRenderers(); + + // always follow in late update, after update modified positions + void LateUpdate() + { + // if owner gets network destroyed for any reason, destroy visual + if (target == null || target.gameObject == null) Destroy(gameObject); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta new file mode 100644 index 000000000..43d3900bd --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 62e7e9424c7e48d69b6a3517796142a1 +timeCreated: 1705235542 \ No newline at end of file diff --git a/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat new file mode 100644 index 000000000..d652a5016 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat @@ -0,0 +1,85 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RemoteGhostMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - _ALPHAPREMULTIPLY_ON + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 3000 + stringTagMap: + RenderType: Transparent + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 10 + - _GlossMapScale: 1 + - _Glossiness: 0.92 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 3 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 0.09849727, g: 1, b: 0, a: 0.15686275} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta new file mode 100644 index 000000000..50854eb07 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 04f0b2088c857414393bab3b80356776 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: From f963f0eb9e6328845308e1a0b8da913b0bc83eda Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sun, 14 Jan 2024 08:54:51 -0500 Subject: [PATCH 06/22] fix(NetworkMatch): Added read only display of MatchID --- .../Components/InterestManagement/Match/NetworkMatch.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs index 970c9a1cc..0d55fd015 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs @@ -11,6 +11,12 @@ public class NetworkMatch : NetworkBehaviour { Guid _matchId; +#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes + [SerializeField, ReadOnly] + [Tooltip("Match ID is shown here on server for debugging purposes.")] + string MatchID = string.Empty; +#pragma warning restore IDE0052 + ///Set this to the same value on all networked objects that belong to a given match public Guid matchId { @@ -25,6 +31,7 @@ public Guid matchId Guid oldMatch = _matchId; _matchId = value; + MatchID = value.ToString(); // Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement) From 3719b25d3d6d2f840aa2b263dea1f6eb227c7581 Mon Sep 17 00:00:00 2001 From: mischa Date: Mon, 15 Jan 2024 10:42:36 +0100 Subject: [PATCH 07/22] Prediction: snapping now applies velocity. fixes stop-start-stop effect for final slow movements --- .../Components/PredictedRigidbody/PredictedRigidbody.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index ccfdeb8a7..126e04a50 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -397,9 +397,13 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) { Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); stateHistory.Clear(); + // apply server state immediately. + // important to apply velocity as well, instead of Vector3.zero. + // in case an object is still slightly moving, we don't want it + // to stop and start moving again on client - slide as well here. physicsCopyRigidbody.position = position; physicsCopyRigidbody.rotation = rotation; - physicsCopyRigidbody.velocity = Vector3.zero; + physicsCopyRigidbody.velocity = velocity; // user callback OnSnappedIntoPlace(); From 552ea5a6b15c2a9a64c34c9c62edd84e19ab2083 Mon Sep 17 00:00:00 2001 From: mischa Date: Mon, 15 Jan 2024 10:43:09 +0100 Subject: [PATCH 08/22] Prediction: snapping now inserts the snapped state into history for more accurate corrections afterward --- .../PredictedRigidbody/PredictedRigidbody.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 126e04a50..e3d85a3d7 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -388,7 +388,7 @@ void RecordState() protected virtual void OnSnappedIntoPlace() {} protected virtual void OnCorrected() {} - void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) + void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity) { // fix rigidbodies seemingly dancing in place instead of coming to rest. // hard snap to the position below a threshold velocity. @@ -396,7 +396,6 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) if (physicsCopyRigidbody.velocity.magnitude <= snapThreshold) { Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); - stateHistory.Clear(); // apply server state immediately. // important to apply velocity as well, instead of Vector3.zero. // in case an object is still slightly moving, we don't want it @@ -405,6 +404,16 @@ void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity) physicsCopyRigidbody.rotation = rotation; physicsCopyRigidbody.velocity = velocity; + // clear history and insert the exact state we just applied. + // this makes future corrections more accurate. + stateHistory.Clear(); + stateHistory.Add(timestamp, new RigidbodyState( + timestamp, + Vector3.zero, position, + Quaternion.identity, rotation, + Vector3.zero, velocity + )); + // user callback OnSnappedIntoPlace(); return; @@ -487,7 +496,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) if (state.timestamp < oldest.timestamp) { Debug.LogWarning($"Hard correcting client because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind."); - ApplyState(state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity); return; } @@ -508,7 +517,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) { double ahead = state.timestamp - newest.timestamp; Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); - ApplyState(state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity); } return; } @@ -519,7 +528,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // something went very wrong. sampling should've worked. // hard correct to recover the error. Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history."); - ApplyState(state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity); return; } @@ -552,7 +561,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // int correctedAmount = stateHistory.Count - afterIndex; // Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); - ApplyState(recomputed.position, recomputed.rotation, recomputed.velocity); + ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity); // user callback OnCorrected(); From 98061c0f590e3b112bf900325bafa02fa59634a6 Mon Sep 17 00:00:00 2001 From: mischa Date: Mon, 15 Jan 2024 15:46:03 +0100 Subject: [PATCH 09/22] Prediction: increase default snap threshold from 0.5 to 2.0 to reduce jitter/fighting at 500ms RTT before objects are coming to rest --- .../Components/PredictedRigidbody/PredictedRigidbody.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index e3d85a3d7..40675b228 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -53,8 +53,8 @@ public class PredictedRigidbody : NetworkBehaviour [Tooltip("Configure how to apply the corrected state.")] public CorrectionMode correctionMode = CorrectionMode.Move; - [Tooltip("Server & Client would sometimes fight over the final position at rest. Instead, hard snap into black below a certain velocity threshold.")] - public float snapThreshold = 0.5f; // adjust with log messages ('snap'). '2' works, but '0.5' is fine too. + [Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")] + public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal. [Header("Visual Interpolation")] [Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")] From 1791d29da2a5d48b304a32dcc780c6ad4cde0439 Mon Sep 17 00:00:00 2001 From: mischa Date: Mon, 15 Jan 2024 21:13:11 +0100 Subject: [PATCH 10/22] Prediction: force syncInterval=0 for now to reduce risks of projects misconfiguring this --- .../Components/PredictedRigidbody/PredictedRigidbody.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 40675b228..70fa272c8 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -626,6 +626,11 @@ protected override void OnValidate() // force syncDirection to be ServerToClient syncDirection = SyncDirection.ServerToClient; + + // state should be synced immediately for now. + // later when we have prediction fully dialed in, + // then we can maybe relax this a bit. + syncInterval = 0; } } } From e1a1f49d0c830c7c889cd44825de0a4aaea868a1 Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Fri, 19 Jan 2024 05:07:33 -0500 Subject: [PATCH 11/22] fix: NetworkServer and NetworkClient respect exceptionDisconnect (#3737) Throws error for disconnect, warning otherwise. --- Assets/Mirror/Core/NetworkClient.cs | 30 +++++++++++++++++----- Assets/Mirror/Core/NetworkServer.cs | 40 ++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index d5d8ddaae..dd83f42ee 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -318,8 +318,14 @@ internal static void OnTransportData(ArraySegment data, int channelId) // always process all messages in the batch. if (!unbatcher.AddBatch(data)) { - Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting."); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: failed to add batch, disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkClient: failed to add batch."); + return; } @@ -355,17 +361,27 @@ internal static void OnTransportData(ArraySegment data, int channelId) // so we need to disconnect. // -> return to avoid the below unbatches.count error. // we already disconnected and handled it. - Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting."); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: failed to unpack and invoke message. Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkClient: failed to unpack and invoke message."); + return; } } // otherwise disconnect else { - // WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)"); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning("NetworkClient: received Message was too short (messages should start with message id)"); return; } } diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 7ae96d821..7af6e5233 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -384,8 +384,13 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta // failure to deserialize disconnects to prevent exploits. if (!identity.DeserializeServer(reader)) { - Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}."); } } } @@ -725,8 +730,14 @@ internal static void OnTransportData(int connectionId, ArraySegment data, // always process all messages in the batch. if (!connection.unbatcher.AddBatch(data)) { - Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)"); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id)."); + return; } @@ -762,17 +773,28 @@ internal static void OnTransportData(int connectionId, ArraySegment data, // so we need to disconnect. // -> return to avoid the below unbatches.count error. // we already disconnected and handled it. - Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}."); + return; } } // otherwise disconnect else { - // WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}"); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id)."); + return; } } From ac050a804d138e377a56618f652fba23d443d32e Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:39:05 -0500 Subject: [PATCH 12/22] fix(NetworkServerTest): Updated tests (#3739) - Expect Errors from PR #3737 --- Assets/Mirror/Tests/Editor/NetworkServer/NetworkServerTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Mirror/Tests/Editor/NetworkServer/NetworkServerTest.cs b/Assets/Mirror/Tests/Editor/NetworkServer/NetworkServerTest.cs index 164c57bdd..6031dafc5 100644 --- a/Assets/Mirror/Tests/Editor/NetworkServer/NetworkServerTest.cs +++ b/Assets/Mirror/Tests/Editor/NetworkServer/NetworkServerTest.cs @@ -632,6 +632,7 @@ public void Send_ClientToServerMessage_UnknownMessageIdDisconnects() // send a message without a registered handler NetworkClient.Send(new TestMessage1()); + LogAssert.Expect(LogType.Error, $"NetworkServer: failed to unpack and invoke message. Disconnecting 1."); ProcessMessages(); // should have been disconnected @@ -684,6 +685,7 @@ public void Send_ServerToClientMessage_UnknownMessageIdDisconnects() // send a message without a registered handler connectionToClient.Send(new TestMessage1()); + LogAssert.Expect(LogType.Error, $"NetworkClient: failed to unpack and invoke message. Disconnecting."); ProcessMessages(); // should have been disconnected @@ -866,6 +868,7 @@ public void UnregisterHandler() // unregister, send again, should not be called again NetworkServer.UnregisterHandler(); NetworkClient.Send(new TestMessage1()); + LogAssert.Expect(LogType.Error, $"NetworkServer: failed to unpack and invoke message. Disconnecting 1."); ProcessMessages(); Assert.That(variant1Called, Is.EqualTo(1)); } @@ -889,6 +892,7 @@ public void ClearHandler() // clear handlers, send again, should not be called again NetworkServer.ClearHandlers(); NetworkClient.Send(new TestMessage1()); + LogAssert.Expect(LogType.Error, $"NetworkServer: failed to unpack and invoke message. Disconnecting 1."); ProcessMessages(); Assert.That(variant1Called, Is.EqualTo(1)); } From b885db37035f4f6a58a111a8e0a57806a6f5945b Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:23:35 -0500 Subject: [PATCH 13/22] fix(NetworkTransform): Separate ResetState (#3738) We should not have hijacked Unity's callback for runtime resets. --- .../NetworkTransform/NetworkTransformBase.cs | 19 ++++++++++++------- .../NetworkTransformReliable.cs | 4 ++-- .../NetworkTransformUnreliable.cs | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs index a60df5932..e6127e83b 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -311,9 +311,9 @@ public void RpcTeleport(Vector3 destination, Quaternion rotation) } [ClientRpc] - void RpcReset() + void RpcResetState() { - Reset(); + ResetState(); } // common Teleport code for client->server and server->client @@ -367,7 +367,7 @@ protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) // -> maybe add destination as first entry? } - public virtual void Reset() + public virtual void ResetState() { // disabled objects aren't updated anymore. // so let's clear the buffers. @@ -375,9 +375,14 @@ public virtual void Reset() clientSnapshots.Clear(); } + public virtual void Reset() + { + ResetState(); + } + protected virtual void OnEnable() { - Reset(); + ResetState(); if (NetworkServer.active) NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged; @@ -385,7 +390,7 @@ protected virtual void OnEnable() protected virtual void OnDisable() { - Reset(); + ResetState(); if (NetworkServer.active) NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged; @@ -403,8 +408,8 @@ void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity id if (syncDirection == SyncDirection.ClientToServer) { - Reset(); - RpcReset(); + ResetState(); + RpcResetState(); } } diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs index e24bb9d0c..f6e851bf0 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -402,9 +402,9 @@ static void RewriteHistory( // 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 Reset() + public override void ResetState() { - base.Reset(); + base.ResetState(); // reset delta lastSerializedPosition = Vector3Long.zero; diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs index 1c23b0e0a..677904d4b 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -346,7 +346,7 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval; if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) - Reset(); + ResetState(); } AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); @@ -401,7 +401,7 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval; if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) - Reset(); + ResetState(); } AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); From 87979aeb384b86d2c83ea73e60ad7f0308823cd0 Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:30:05 -0500 Subject: [PATCH 14/22] fix: Updated Network Transform Template --- ...twork Transform-NewNetworkTransform.cs.txt | 116 ++++++++---------- 1 file changed, 51 insertions(+), 65 deletions(-) diff --git a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt index 5b0cb269e..3f06e76b0 100644 --- a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt +++ b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt @@ -10,20 +10,10 @@ using Mirror; public class #SCRIPTNAME# : NetworkTransformBase { - protected override Transform targetComponent => transform; - - // If you need this template to reference a child target, - // replace the line above with the code below. - - /* - [Header("Target")] - public Transform target; - - protected override Transform targetComponent => target; - */ - #region Unity Callbacks + protected override void Awake() { } + protected override void OnValidate() { base.OnValidate(); @@ -45,44 +35,56 @@ public class #SCRIPTNAME# : NetworkTransformBase base.OnDisable(); } - /// - /// Buffers are cleared and interpolation times are reset to zero here. - /// This may be called when you are implementing some system of not sending - /// if nothing changed, or just plain resetting if you have not received data - /// for some time, as this will prevent a long interpolation period between old - /// and just received data, as it will look like a lag. Reset() should also be - /// called when authority is changed to another client or server, to prevent - /// old buffers bugging out the interpolation if authority is changed back. - /// - public override void Reset() - { - base.Reset(); - } - #endregion #region NT Base Callbacks /// - /// NTSnapshot struct is created from incoming data from server - /// and added to SnapshotInterpolation sorted list. - /// You may want to skip calling the base method for the local player - /// if doing client-side prediction, or perhaps pass altered values, - /// or compare the server data to local values and correct large differences. + /// NTSnapshot struct is created here /// - protected override void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + protected override TransformSnapshot Construct() { - base.OnServerToClientSync(position, rotation, scale); + return base.Construct(); + } + + protected override Vector3 GetPosition() + { + return base.GetPosition(); + } + + protected override Quaternion GetRotation() + { + return base.GetRotation(); + } + + protected override Vector3 GetScale() + { + return base.GetScale(); + } + + protected override void SetPosition(Vector3 position) + { + base.SetPosition(position); + } + + protected override void SetRotation(Quaternion rotation) + { + base.SetRotation(rotation); + } + + protected override void SetScale(Vector3 scale) + { + base.SetScale(scale); } /// - /// NTSnapshot struct is created from incoming data from client - /// and added to SnapshotInterpolation sorted list. - /// You may want to implement anti-cheat checks here in client authority mode. + /// localPosition, localRotation, and localScale are set here: + /// interpolated values are used if interpolation is enabled. + /// goal values are used if interpolation is disabled. /// - protected override void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + protected override void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal) { - base.OnClientToServerSync(position, rotation, scale); + base.Apply(interpolated, endGoal); } /// @@ -106,48 +108,32 @@ public class #SCRIPTNAME# : NetworkTransformBase } /// - /// NTSnapshot struct is created here + /// Buffers are cleared and interpolation times are reset to zero here. + /// This may be called when you are implementing some system of not sending + /// if nothing changed, or just plain resetting if you have not received data + /// for some time, as this will prevent a long interpolation period between old + /// and just received data, as it will look like a lag. Reset() should also be + /// called when authority is changed to another client or server, to prevent + /// old buffers bugging out the interpolation if authority is changed back. /// - protected override NTSnapshot ConstructSnapshot() + public override void ResetState() { - return base.ConstructSnapshot(); + base.ResetState(); } - /// - /// localPosition, localRotation, and localScale are set here: - /// interpolated values are used if interpolation is enabled. - /// goal values are used if interpolation is disabled. - /// - protected override void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal) - { - base.Apply(interpolated, endGoal); - } - -#if onlySyncOnChange_BANDWIDTH_SAVING - - /// - /// Returns true if position, rotation AND scale are unchanged, within given sensitivity range. - /// - protected override bool CompareSnapshots(NTSnapshot currentSnapshot) - { - return base.CompareSnapshots(currentSnapshot); - } - -#endif - #endregion #region GUI -#if UNITY_EDITOR || DEVELOPMENT_BUILD // OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD protected override void OnGUI() { base.OnGUI(); } - protected override void DrawGizmos(SortedList buffer) + protected override void DrawGizmos(SortedList buffer) { base.DrawGizmos(buffer); } From dd1a457f714bfb900ce923ed5044a81c00b42bbd Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sat, 20 Jan 2024 04:46:13 -0500 Subject: [PATCH 15/22] fix(NetworkIdentity): Separate ResetState (#3742) * fix(NetworkTransform): Separate ResetState We should not have hijacked Unity's callback for runtime resets. * Rename test too --- Assets/Mirror/Core/NetworkClient.cs | 8 ++++---- Assets/Mirror/Core/NetworkIdentity.cs | 2 +- Assets/Mirror/Core/NetworkServer.cs | 2 +- .../NetworkIdentity/NetworkIdentitySerializationTests.cs | 2 +- .../Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index dd83f42ee..c7699a059 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -1650,7 +1650,7 @@ public static void DestroyAllClientObjects() // unspawned objects should be reset for reuse later. if (wasUnspawned) { - identity.Reset(); + identity.ResetState(); } // without unspawn handler, we need to disable/destroy. else @@ -1659,7 +1659,7 @@ public static void DestroyAllClientObjects() // they always stay in the scene, we don't destroy them. if (identity.sceneId != 0) { - identity.Reset(); + identity.ResetState(); identity.gameObject.SetActive(false); } // spawned objects are destroyed @@ -1695,7 +1695,7 @@ static void DestroyObject(uint netId) if (InvokeUnSpawnHandler(identity.assetId, identity.gameObject)) { // reset object after user's handler - identity.Reset(); + identity.ResetState(); } // otherwise fall back to default Destroy else if (identity.sceneId == 0) @@ -1709,7 +1709,7 @@ static void DestroyObject(uint netId) identity.gameObject.SetActive(false); spawnableObjects[identity.sceneId] = identity; // reset for scene objects - identity.Reset(); + identity.ResetState(); } // remove from dictionary no matter how it is unspawned diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs index 8248988cb..086c0a645 100644 --- a/Assets/Mirror/Core/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -1286,7 +1286,7 @@ public void RemoveClientAuthority() // the identity during destroy as people might want to be able to read // the members inside OnDestroy(), and we have no way of invoking reset // after OnDestroy is called. - internal void Reset() + internal void ResetState() { hasSpawned = false; clientStarted = false; diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 7af6e5233..d6e2fcfee 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -1657,7 +1657,7 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // otherwise simply .Reset() and set inactive again else if (mode == DestroyMode.Reset) { - identity.Reset(); + identity.ResetState(); } } diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs index 9d466f026..25e8c8dc9 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs @@ -140,7 +140,7 @@ public void TooManyComponents() // let's reset and initialize again with the added ones. // this should show the 'too many components' error LogAssert.Expect(LogType.Error, new Regex(".*too many NetworkBehaviour.*")); - serverIdentity.Reset(); + serverIdentity.ResetState(); // clientIdentity.Reset(); serverIdentity.Awake(); // clientIdentity.Awake(); diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs index e627c6f49..e6632a5c5 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentityTests.cs @@ -624,7 +624,7 @@ public void ClearAllComponentsDirtyBits() } [Test] - public void Reset() + public void ResetState() { CreateNetworked(out GameObject _, out NetworkIdentity identity); @@ -637,7 +637,7 @@ public void Reset() identity.observers[43] = new NetworkConnectionToClient(2); // mark for reset and reset - identity.Reset(); + identity.ResetState(); Assert.That(identity.isServer, Is.False); Assert.That(identity.isClient, Is.False); Assert.That(identity.isLocalPlayer, Is.False); From e10ab75bf511c0578ed38d3b771173d4cc725868 Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sat, 20 Jan 2024 04:46:29 -0500 Subject: [PATCH 16/22] fix(InterestManagement): Separate ResetState (#3743) * fix(InterestManagement): Separate ResetState We should not have hijacked Unity's callback for runtime resets. * Updated Template --- .../InterestManagement/Distance/DistanceInterestManagement.cs | 2 +- .../SpatialHashing/SpatialHashingInterestManagement.cs | 2 +- Assets/Mirror/Core/InterestManagementBase.cs | 2 +- Assets/Mirror/Core/NetworkServer.cs | 4 ++-- ...Custom Interest Management-CustomInterestManagement.cs.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs index 044155877..654996191 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs @@ -25,7 +25,7 @@ int GetVisRange(NetworkIdentity identity) } [ServerCallback] - public override void Reset() + public override void ResetState() { lastRebuildTime = 0D; CustomRanges.Clear(); diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index 298603497..066a1c5ee 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -72,7 +72,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet [ServerCallback] - public override void Reset() { } + public override void ResetState() { } [ServerCallback] void Update() From 258a84c066f8b54399f4723b2215ba38e0001e2d Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sat, 20 Jan 2024 04:47:41 -0500 Subject: [PATCH 17/22] feat(NetworkTransform): Default Sync Direction = Client To Server (#3741) * feat(NetworkTransform): Default Sync Direction = Client To Server - This is done in Reset so Network Behaviour isn't effected. - This will not change NT components already in place on objects / prefabs unless user manually chooses Reset in the inspector. * Update Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs --------- Co-authored-by: mischa <16416509+miwarnec@users.noreply.github.com> --- .../Mirror/Components/NetworkTransform/NetworkTransformBase.cs | 2 ++ .../Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs index e6127e83b..e752c2020 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -378,6 +378,8 @@ public virtual void ResetState() public virtual void Reset() { ResetState(); + // default to ClientToServer so this works immediately for users + syncDirection = SyncDirection.ClientToServer; } protected virtual void OnEnable() diff --git a/Assets/Mirror/Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs b/Assets/Mirror/Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs index dcc43825b..8ea890997 100644 --- a/Assets/Mirror/Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkTransform/NetworkTransform2kTests.cs @@ -314,6 +314,9 @@ public void OnServerToClientSync_WithClientAuthority_Nullables_Uses_Last() component.netIdentity.isClient = true; component.netIdentity.isLocalPlayer = true; + // client authority has to be disabled + component.syncDirection = SyncDirection.ServerToClient; + // call OnClientToServerSync with authority and nullable types // to make sure it uses the last valid position then. component.OnServerToClientSync(new Vector3?(), new Quaternion?(), new Vector3?()); From ac344fd9d1577b9ca466f76fcc0a9d7e95278045 Mon Sep 17 00:00:00 2001 From: mischa Date: Sun, 21 Jan 2024 12:13:29 +0100 Subject: [PATCH 18/22] Prediction: detect renderers in children --- .../Components/PredictedRigidbody/PredictedRigidbody.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 70fa272c8..cb47262c6 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -164,10 +164,13 @@ protected virtual void MoveMeshColliders(GameObject destination) protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) { - if (TryGetComponent(out MeshRenderer originalMeshRenderer)) + // find the MeshRenderer component, which sometimes is on a child. + MeshRenderer originalMeshRenderer = GetComponentInChildren(true); + MeshFilter originalMeshFilter = GetComponentInChildren(true); + if (originalMeshRenderer != null && originalMeshFilter != null) { MeshFilter meshFilter = destination.AddComponent(); - meshFilter.mesh = GetComponent().mesh; + meshFilter.mesh = originalMeshFilter.mesh; MeshRenderer meshRenderer = destination.AddComponent(); meshRenderer.material = originalMeshRenderer.material; From d97739df7127a51151d9629ac23500ea1aaea526 Mon Sep 17 00:00:00 2001 From: Justin Nolan Date: Sun, 21 Jan 2024 14:55:37 +0100 Subject: [PATCH 19/22] Add missing replace handler override to NetworkServer (#3744) --- Assets/Mirror/Core/NetworkServer.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index c7ba80e89..8cb713604 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -930,6 +930,14 @@ public static void ReplaceHandler(Action handle ushort msgType = NetworkMessageId.Id; handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } + + /// Replace a handler for message type T. Most should require authentication. + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + } /// Unregister a handler for a message type T. public static void UnregisterHandler() From b69b5ae6044a3fe76bd8edbf36e16c0557163043 Mon Sep 17 00:00:00 2001 From: JesusLuvsYooh <57072365+JesusLuvsYooh@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:02:42 +0000 Subject: [PATCH 20/22] perf(BenchmarkIdle Example): Lowered spawn amount to 10k (#3745) 50k causes a long delay, and looks like Unity has initially frozen, even on beefy machines. To prevent users from thinking this, and perhaps force closing, amount is lowered to something still crazy, but more reasonable. :) - Nothing else has been touched --- Assets/Mirror/Examples/BenchmarkIdle/MirrorBenchmarkIdle.unity | 1 + 1 file changed, 1 insertion(+) diff --git a/Assets/Mirror/Examples/BenchmarkIdle/MirrorBenchmarkIdle.unity b/Assets/Mirror/Examples/BenchmarkIdle/MirrorBenchmarkIdle.unity index 147bb8048..bae7496f7 100644 --- a/Assets/Mirror/Examples/BenchmarkIdle/MirrorBenchmarkIdle.unity +++ b/Assets/Mirror/Examples/BenchmarkIdle/MirrorBenchmarkIdle.unity @@ -367,6 +367,7 @@ MonoBehaviour: connectionQualityInterval: 3 timeInterpolationGui: 0 spawnAmount: 50000 + spawnAmount: 10000 interleave: 2 spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344, type: 3} From 4c0d979d4d6e77c479668a827a619ea5c28ea9a9 Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Sun, 21 Jan 2024 16:18:56 -0500 Subject: [PATCH 21/22] fix(Room Example): Updated Reward Script - Added Headers - made `available` private with ReadOnly --- Assets/Mirror/Examples/Room/Scripts/Reward.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Assets/Mirror/Examples/Room/Scripts/Reward.cs b/Assets/Mirror/Examples/Room/Scripts/Reward.cs index 989b6e6c1..ac2ced195 100644 --- a/Assets/Mirror/Examples/Room/Scripts/Reward.cs +++ b/Assets/Mirror/Examples/Room/Scripts/Reward.cs @@ -5,9 +5,13 @@ namespace Mirror.Examples.NetworkRoom [RequireComponent(typeof(Common.RandomColor))] public class Reward : NetworkBehaviour { - public bool available = true; + [Header("Components")] public Common.RandomColor randomColor; + [Header("Diagnostics")] + [ReadOnly, SerializeField] + bool available = true; + protected override void OnValidate() { base.OnValidate(); From 50d0008d03a3c547cef67bd6e16cb8a6c9581cce Mon Sep 17 00:00:00 2001 From: MrGadget <9826063+MrGadget1024@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:45:32 -0500 Subject: [PATCH 22/22] feat(NetworkClient): Add ReplaceHandler with channel param (#3747) * breaking(NetworkClient): Remove NetworkConnection parameter from ReplaceHandler There is only one connection on client. Aligns with RegisterHandler that takes no NetworkConnection parameter. * feat(NetworkClient): Add ReplaceHandler with channel param --- Assets/Mirror/Core/NetworkClient.cs | 32 ++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index c7699a059..e1362c1a5 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -552,14 +552,17 @@ public static void RegisterHandler(Action handler, bool requireAuthen handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } - /// Replace a handler for a particular message type. Should require authentication by default. - // RegisterHandler throws a warning (as it should) if a handler is assigned twice - // Use of ReplaceHandler makes it clear the user intended to replace the handler + // Deprecated 2024-01-21 + [Obsolete("Use ReplaceHandler without the NetworkConnection parameter instead. This version is obsolete and will be removed soon.")] public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. ushort msgType = NetworkMessageId.Id; - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + void HandlerWrapped(NetworkConnection _, T value) => handler(_, value); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } /// Replace a handler for a particular message type. Should require authentication by default. @@ -568,7 +571,26 @@ public static void ReplaceHandler(Action handler, bool public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication); + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. + ushort msgType = NetworkMessageId.Id; + void HandlerWrapped(NetworkConnection _, T value) => handler(value); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); + } + + /// Replace a handler for a particular message type. Should require authentication by default. This version passes channelId to the handler. + // RegisterHandler throws a warning (as it should) if a handler is assigned twice + // Use of ReplaceHandler makes it clear the user intended to replace the handler + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. + ushort msgType = NetworkMessageId.Id; + void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } /// Unregister a message handler of type T.