diff --git a/.github/workflows/RunUnityTests.yml b/.github/workflows/RunUnityTests.yml index cdfa163d3..d8b9100cd 100644 --- a/.github/workflows/RunUnityTests.yml +++ b/.github/workflows/RunUnityTests.yml @@ -14,8 +14,8 @@ jobs: - 2019.4.40f1 - 2020.3.48f1 - 2021.3.33f1 - - 2022.3.16f1 - - 2023.2.4f1 + - 2022.3.18f1 + - 2023.2.7f1 steps: - name: Checkout repository @@ -47,7 +47,7 @@ jobs: customParameters: -stackTraceLogType None - name: Archive test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: Test Results ${{ matrix.unityVersion }} diff --git a/.github/workflows/Semantic.yml b/.github/workflows/Semantic.yml index 0218bc335..4a6a0fec8 100644 --- a/.github/workflows/Semantic.yml +++ b/.github/workflows/Semantic.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -32,7 +32,7 @@ jobs: - name: Package run: unity-packer pack Mirror.unitypackage Assets/Mirror Assets/Mirror Assets/ScriptTemplates Assets/ScriptTemplates LICENSE Assets/Mirror/LICENSE - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: Mirror.unitypackage path: Mirror.unitypackage diff --git a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs index ee02a0790..290eb0698 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs +++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs @@ -1,4 +1,5 @@ // simple component that holds team information +using System; using UnityEngine; namespace Mirror @@ -8,10 +9,31 @@ namespace Mirror [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] public class NetworkTeam : NetworkBehaviour { - [Tooltip("Set this to the same value on all networked objects that belong to a given team")] - [SyncVar] public string teamId = string.Empty; + [SerializeField] + [Tooltip("Set teamId on Server at runtime to the same value on all networked objects that belong to a given team")] + string _teamId; + + public string teamId + { + get => _teamId; + set + { + if (Application.IsPlaying(gameObject) && !NetworkServer.active) + throw new InvalidOperationException("teamId can only be set at runtime on active server"); + + if (_teamId == value) + return; + + string oldTeam = _teamId; + _teamId = value; + + //Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement + if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement) + teamInterestManagement.OnTeamChanged(this, oldTeam); + } + } [Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")] - [SyncVar] public bool forceShown; + public bool forceShown; } } diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs index 7701c38d0..cb5bdb7d9 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using UnityEngine; namespace Mirror @@ -6,126 +6,112 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")] public class TeamInterestManagement : InterestManagement { - readonly Dictionary> teamObjects = new Dictionary>(); - readonly Dictionary lastObjectTeam = new Dictionary(); + readonly Dictionary> teamObjects = + new Dictionary>(); + readonly HashSet dirtyTeams = new HashSet(); + // LateUpdate so that all spawns/despawns/changes are done + [ServerCallback] + void LateUpdate() + { + // Rebuild all dirty teams + // dirtyTeams will be empty if no teams changed members + // by spawning or destroying or changing teamId in this frame. + foreach (string dirtyTeam in dirtyTeams) + { + // rebuild always, even if teamObjects[dirtyTeam] is empty. + // Players might have left the team, but they may still be spawned. + RebuildTeamObservers(dirtyTeam); + + // clean up empty entries in the dict + if (teamObjects[dirtyTeam].Count == 0) + teamObjects.Remove(dirtyTeam); + } + + dirtyTeams.Clear(); + } + + [ServerCallback] + void RebuildTeamObservers(string teamId) + { + foreach (NetworkTeam networkTeam in teamObjects[teamId]) + if (networkTeam.netIdentity != null) + NetworkServer.RebuildObservers(networkTeam.netIdentity, false); + } + + // called by NetworkTeam.teamId setter + [ServerCallback] + internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam) + { + // This object is in a new team so observers in the prior team + // and the new team need to rebuild their respective observers lists. + + // Remove this object from the hashset of the team it just left + // Null / Empty string is never a valid teamId + if (!string.IsNullOrWhiteSpace(oldTeam)) + { + dirtyTeams.Add(oldTeam); + teamObjects[oldTeam].Remove(networkTeam); + } + + // Null / Empty string is never a valid teamId + if (string.IsNullOrWhiteSpace(networkTeam.teamId)) + return; + + dirtyTeams.Add(networkTeam.teamId); + + // Make sure this new team is in the dictionary + if (!teamObjects.ContainsKey(networkTeam.teamId)) + teamObjects[networkTeam.teamId] = new HashSet(); + + // Add this object to the hashset of the new team + teamObjects[networkTeam.teamId].Add(networkTeam); + } + [ServerCallback] public override void OnSpawned(NetworkIdentity identity) { - if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) return; - string networkTeamId = identityNetworkTeam.teamId; - lastObjectTeam[identity] = networkTeamId; + string networkTeamId = networkTeam.teamId; // Null / Empty string is never a valid teamId...do not add to teamObjects collection if (string.IsNullOrWhiteSpace(networkTeamId)) return; - //Debug.Log($"TeamInterestManagement.OnSpawned {identity.name} {networkTeamId}"); - - if (!teamObjects.TryGetValue(networkTeamId, out HashSet objects)) + // Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}"); + if (!teamObjects.TryGetValue(networkTeamId, out HashSet objects)) { - objects = new HashSet(); + objects = new HashSet(); teamObjects.Add(networkTeamId, objects); } - objects.Add(identity); + objects.Add(networkTeam); // Team ID could have been set in NetworkBehaviour::OnStartServer on this object. // Since that's after OnCheckObserver is called it would be missed, so force Rebuild here. - // Add the current team to dirtyTeams for Update to rebuild it. + // Add the current team to dirtyTeames for LateUpdate to rebuild it. dirtyTeams.Add(networkTeamId); } [ServerCallback] public override void OnDestroyed(NetworkIdentity identity) { - // Don't RebuildSceneObservers here - that will happen in Update. + // Don't RebuildSceneObservers here - that will happen in LateUpdate. // Multiple objects could be destroyed in same frame and we don't - // want to rebuild for each one...let Update do it once. - // We must add the current team to dirtyTeams for Update to rebuild it. - if (lastObjectTeam.TryGetValue(identity, out string currentTeam)) + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current team to dirtyTeames for LateUpdate to rebuild it. + if (identity.TryGetComponent(out NetworkTeam currentTeam)) { - lastObjectTeam.Remove(identity); - if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity)) - dirtyTeams.Add(currentTeam); + if (!string.IsNullOrWhiteSpace(currentTeam.teamId) && + teamObjects.TryGetValue(currentTeam.teamId, out HashSet objects) && + objects.Remove(currentTeam)) + dirtyTeams.Add(currentTeam.teamId); } } - // internal so we can update from tests - [ServerCallback] - internal void Update() - { - // for each spawned: - // if team changed: - // add previous to dirty - // add new to dirty - foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) - { - // Ignore objects that don't have a NetworkTeam component - if (!netIdentity.TryGetComponent(out NetworkTeam identityNetworkTeam)) - continue; - - string networkTeamId = identityNetworkTeam.teamId; - if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam)) - continue; - - // Null / Empty string is never a valid teamId - // Nothing to do if teamId hasn't changed - if (string.IsNullOrWhiteSpace(networkTeamId) || networkTeamId == currentTeam) - continue; - - // Mark new/old Teams as dirty so they get rebuilt - UpdateDirtyTeams(networkTeamId, currentTeam); - - // This object is in a new team so observers in the prior team - // and the new team need to rebuild their respective observers lists. - UpdateTeamObjects(netIdentity, networkTeamId, currentTeam); - } - - // rebuild all dirty teams - foreach (string dirtyTeam in dirtyTeams) - RebuildTeamObservers(dirtyTeam); - - dirtyTeams.Clear(); - } - - void UpdateDirtyTeams(string newTeam, string currentTeam) - { - // Null / Empty string is never a valid teamId - if (!string.IsNullOrWhiteSpace(currentTeam)) - dirtyTeams.Add(currentTeam); - - dirtyTeams.Add(newTeam); - } - - void UpdateTeamObjects(NetworkIdentity netIdentity, string newTeam, string currentTeam) - { - // Remove this object from the hashset of the team it just left - // string.Empty is never a valid teamId - if (!string.IsNullOrWhiteSpace(currentTeam)) - teamObjects[currentTeam].Remove(netIdentity); - - // Set this to the new team this object just entered - lastObjectTeam[netIdentity] = newTeam; - - // Make sure this new team is in the dictionary - if (!teamObjects.ContainsKey(newTeam)) - teamObjects.Add(newTeam, new HashSet()); - - // Add this object to the hashset of the new team - teamObjects[newTeam].Add(netIdentity); - } - - void RebuildTeamObservers(string teamId) - { - foreach (NetworkIdentity netIdentity in teamObjects[teamId]) - if (netIdentity != null) - NetworkServer.RebuildObservers(netIdentity, false); - } - public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Always observed if no NetworkTeam component @@ -135,7 +121,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection if (identityNetworkTeam.forceShown) return true; - // string.Empty is never a valid teamId + // Null / Empty string is never a valid teamId if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId)) return false; @@ -149,7 +135,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection //Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}"); - // Observed only if teamId's match + // Observed only if teamId's team return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId; } @@ -173,14 +159,14 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet objects)) + // Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged + if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet objects)) return; // Add everything in the hashset for this object's current team - foreach (NetworkIdentity networkIdentity in objects) - if (networkIdentity != null && networkIdentity.connectionToClient != null) - newObservers.Add(networkIdentity.connectionToClient); + foreach (NetworkTeam netTeam in objects) + if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null) + newObservers.Add(netTeam.netIdentity.connectionToClient); } void AddAllConnections(HashSet newObservers) diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index cb47262c6..3f86cab6f 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -23,6 +23,7 @@ public enum CorrectionMode // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. public class PredictedRigidbody : NetworkBehaviour { + Transform tf; // this component is performance critical. cache .transform getter! Rigidbody rb; // own rigidbody on server. this is never moved to a physics copy. Vector3 lastPosition; @@ -79,87 +80,19 @@ public class PredictedRigidbody : NetworkBehaviour // Rigidbody & Collider are moved out into a separate object. // this way the visual object can smoothly follow. protected GameObject physicsCopy; + Transform physicsCopyTransform; // caching to avoid GetComponent Rigidbody physicsCopyRigidbody; // caching to avoid GetComponent - Collider physicsCopyCollider; // caching to avoid GetComponent + Collider physicsCopyCollider; // caching to avoid GetComponent + float smoothFollowThreshold; // caching to avoid calculation in LateUpdate // we also create one extra ghost for the exact known server state. protected GameObject remoteCopy; void Awake() { + tf = transform; rb = GetComponent(); - } - - protected virtual Rigidbody MoveRigidbody(GameObject destination) - { - 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; - } - - 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); - } - } - - 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); - } + if (rb == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component."); } protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) @@ -203,25 +136,32 @@ protected virtual void CreateGhosts() Debug.Log($"Separating Physics for {name}"); // create an empty GameObject with the same name + _Physical + // it's important to copy world position/rotation/scale, not local! + // because the original object may be a child of another. + // + // for example: + // parent (scale=1.5) + // child (scale=0.5) + // + // if we copy localScale then the copy has scale=0.5, where as the + // original would have a global scale of ~1.0. physicsCopy = new GameObject($"{name}_Physical"); - physicsCopy.transform.position = transform.position; - physicsCopy.transform.rotation = transform.rotation; - physicsCopy.transform.localScale = transform.localScale; + physicsCopy.transform.position = tf.position; // world position! + physicsCopy.transform.rotation = tf.rotation; // world rotation! + physicsCopy.transform.localScale = tf.lossyScale; // world scale! + + // assign the same Layer for the physics copy. + // games may use a custom physics collision matrix, layer matters. + physicsCopy.layer = gameObject.layer; // add the PredictedRigidbodyPhysical component PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent(); - physicsGhostRigidbody.target = this; + physicsGhostRigidbody.target = tf; physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; - // 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); + // move the rigidbody component & all colliders to the physics GameObject + PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy); // show ghost by copying all renderers / materials with ghost material applied if (showGhost) @@ -232,41 +172,70 @@ protected virtual void CreateGhosts() physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; // one for the latest remote state for comparison + // it's important to copy world position/rotation/scale, not local! + // because the original object may be a child of another. + // + // for example: + // parent (scale=1.5) + // child (scale=0.5) + // + // if we copy localScale then the copy has scale=0.5, where as the + // original would have a global scale of ~1.0. remoteCopy = new GameObject($"{name}_Remote"); + remoteCopy.transform.position = tf.position; // world position! + remoteCopy.transform.rotation = tf.rotation; // world rotation! + remoteCopy.transform.localScale = tf.lossyScale; // world scale! PredictedRigidbodyRemoteGhost predictedGhost = remoteCopy.AddComponent(); - predictedGhost.target = this; + predictedGhost.target = tf; predictedGhost.ghostDistanceThreshold = ghostDistanceThreshold; predictedGhost.ghostEnabledCheckInterval = ghostEnabledCheckInterval; CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial); } // cache components to avoid GetComponent calls at runtime + physicsCopyTransform = physicsCopy.transform; physicsCopyRigidbody = physicsCopy.GetComponent(); - physicsCopyCollider = physicsCopy.GetComponent(); - if (physicsGhostRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); + physicsCopyCollider = physicsCopy.GetComponentInChildren(); + if (physicsCopyRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); if (physicsCopyCollider == null) throw new Exception("SeparatePhysics: couldn't find final Collider."); + + // cache some threshold to avoid calculating them in LateUpdate + float colliderSize = physicsCopyCollider.bounds.size.magnitude; + smoothFollowThreshold = colliderSize * teleportDistanceMultiplier; } - protected virtual void DestroyCopies() + protected virtual void DestroyGhosts() { - if (physicsCopy != null) Destroy(physicsCopy); + // move the copy's Rigidbody back onto self. + // important for scene objects which may be reused for AOI spawn/despawn. + // otherwise next time they wouldn't have a collider anymore. + if (physicsCopy != null) + { + PredictionUtils.MovePhysicsComponents(physicsCopy, gameObject); + Destroy(physicsCopy); + } + + // simply destroy the remote copy if (remoteCopy != null) Destroy(remoteCopy); } + // this shows in profiler LateUpdates! need to make this as fast as possible! protected virtual void SmoothFollowPhysicsCopy() { // hard follow: - // transform.position = physicsCopyCollider.position; - // transform.rotation = physicsCopyCollider.rotation; + // tf.position = physicsCopyCollider.position; + // tf.rotation = physicsCopyCollider.rotation; + // ORIGINAL VERSION: CLEAN AND SIMPLE + /* // 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); + float distance = Vector3.Distance(tf.position, physicsCopyRigidbody.position); if (distance > threshold) { - transform.position = physicsCopyRigidbody.position; - transform.rotation = physicsCopyRigidbody.rotation; + tf.position = physicsCopyRigidbody.position; + tf.rotation = physicsCopyRigidbody.rotation; Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}"); return; } @@ -274,15 +243,42 @@ protected virtual void SmoothFollowPhysicsCopy() // smoothly interpolate to the target position. // speed relative to how far away we are float positionStep = distance * positionInterpolationSpeed; + tf.position = Vector3.MoveTowards(tf.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. + tf.rotation = Quaternion.Slerp(tf.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime); + */ + + // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! + Vector3 currentPosition = tf.position; + Quaternion currentRotation = tf.rotation; + Vector3 physicsPosition = physicsCopyTransform.position; // faster than accessing physicsCopyRigidbody! + Quaternion physicsRotation = physicsCopyTransform.rotation; // faster than accessing physicsCopyRigidbody! + float deltaTime = Time.deltaTime; + + float distance = Vector3.Distance(currentPosition, physicsPosition); + if (distance > smoothFollowThreshold) + { + tf.SetPositionAndRotation(physicsPosition, physicsRotation); // faster than .position and .rotation manually + Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {smoothFollowThreshold:F2}"); + return; + } + + // smoothly interpolate to the target position. // 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); + float positionStep = distance * positionInterpolationSpeed; + Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * 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); + Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime); + + // assign position and rotation together. faster than accessing manually. + tf.SetPositionAndRotation(newPosition, newRotation); } // creater visual copy only on clients, where players are watching. @@ -294,14 +290,17 @@ public override void OnStartClient() // 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() => DestroyCopies(); + public override void OnStopClient() + { + DestroyGhosts(); + } void UpdateServer() { // to save bandwidth, we only serialize when position changed - // if (Vector3.Distance(transform.position, lastPosition) >= positionSensitivity) + // if (Vector3.Distance(tf.position, lastPosition) >= positionSensitivity) // { - // lastPosition = transform.position; + // lastPosition = tf.position; // SetDirty(); // } @@ -398,7 +397,8 @@ void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 // this is fine because the visual object still smoothly interpolates to it. if (physicsCopyRigidbody.velocity.magnitude <= snapThreshold) { - Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); + // Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); + // 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 @@ -448,7 +448,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) { remoteCopy.transform.position = state.position; remoteCopy.transform.rotation = state.rotation; - remoteCopy.transform.localScale = transform.localScale; + remoteCopy.transform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment. } // OPTIONAL performance optimization when comparing idle objects. @@ -498,7 +498,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // otherwise it could be out of sync as long as it's too far behind. 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."); + Debug.LogWarning($"Hard correcting client object {name} 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.timestamp, state.position, state.rotation, state.velocity); return; } diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs index 5e4674987..3168f15bd 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs @@ -7,8 +7,10 @@ namespace Mirror { public class PredictedRigidbodyPhysicsGhost : MonoBehaviour { + // this is performance critical, so store target's .Transform instead of + // PredictedRigidbody, this way we don't need to call the .transform getter. [Tooltip("The predicted rigidbody owner.")] - public PredictedRigidbody target; + public Transform target; // ghost (settings are copyed from PredictedRigidbody) MeshRenderer ghost; @@ -16,13 +18,15 @@ public class PredictedRigidbodyPhysicsGhost : MonoBehaviour public float ghostEnabledCheckInterval = 0.2f; double lastGhostEnabledCheckTime = 0; - // cache + // cache components because this is performance critical! + Transform tf; Collider co; // we add this component manually from PredictedRigidbody. // so assign this in Start. target isn't set in Awake yet. void Start() { + tf = transform; co = GetComponent(); ghost = GetComponent(); } @@ -43,7 +47,7 @@ void UpdateGhostRenderers() // 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; + bool insideTarget = Vector3.Distance(tf.position, target.position) <= ghostDistanceThreshold; ghost.enabled = !insideTarget; } @@ -53,7 +57,7 @@ void UpdateGhostRenderers() void LateUpdate() { // if owner gets network destroyed for any reason, destroy visual - if (target == null || target.gameObject == null) Destroy(gameObject); + if (target == null) Destroy(gameObject); } // also show a yellow gizmo for the predicted & corrected physics. diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs index 24b37b624..4e1127c38 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs @@ -5,8 +5,10 @@ namespace Mirror { public class PredictedRigidbodyRemoteGhost : MonoBehaviour { + // this is performance critical, so store target's .Transform instead of + // PredictedRigidbody, this way we don't need to call the .transform getter. [Tooltip("The predicted rigidbody owner.")] - public PredictedRigidbody target; + public Transform target; // ghost (settings are copyed from PredictedRigidbody) MeshRenderer ghost; @@ -14,10 +16,14 @@ public class PredictedRigidbodyRemoteGhost : MonoBehaviour public float ghostEnabledCheckInterval = 0.2f; double lastGhostEnabledCheckTime = 0; + // cache components because this is performance critical! + Transform tf; + // we add this component manually from PredictedRigidbody. // so assign this in Start. target isn't set in Awake yet. void Start() { + tf = transform; ghost = GetComponent(); } @@ -37,7 +43,7 @@ void UpdateGhostRenderers() // 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; + bool insideTarget = Vector3.Distance(tf.position, target.position) <= ghostDistanceThreshold; ghost.enabled = !insideTarget; } @@ -47,7 +53,7 @@ void UpdateGhostRenderers() void LateUpdate() { // if owner gets network destroyed for any reason, destroy visual - if (target == null || target.gameObject == null) Destroy(gameObject); + if (target == null) Destroy(gameObject); } } } diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta index 43d3900bd..f79f911a0 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 62e7e9424c7e48d69b6a3517796142a1 -timeCreated: 1705235542 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs new file mode 100644 index 000000000..3fd751624 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs @@ -0,0 +1,394 @@ +// standalone utility functions for PredictedRigidbody component. +using System; +using UnityEngine; + +namespace Mirror +{ + public static class PredictionUtils + { + // rigidbody /////////////////////////////////////////////////////////// + // move a Rigidbody + settings from one GameObject to another. + public static void MoveRigidbody(GameObject source, GameObject destination) + { + // create a new Rigidbody component on destination. + // note that adding a Joint automatically adds a Rigidbody. + // so first check if one was added yet. + Rigidbody original = source.GetComponent(); + if (original == null) throw new Exception($"Prediction: attempted to move {source}'s Rigidbody to the predicted copy, but there was no component."); + Rigidbody rigidbodyCopy; + if (!destination.TryGetComponent(out rigidbodyCopy)) + rigidbodyCopy = destination.AddComponent(); + + // copy all properties + rigidbodyCopy.mass = original.mass; + rigidbodyCopy.drag = original.drag; + rigidbodyCopy.angularDrag = original.angularDrag; + rigidbodyCopy.useGravity = original.useGravity; + rigidbodyCopy.isKinematic = original.isKinematic; + rigidbodyCopy.interpolation = original.interpolation; + rigidbodyCopy.collisionDetectionMode = original.collisionDetectionMode; + rigidbodyCopy.constraints = original.constraints; + rigidbodyCopy.sleepThreshold = original.sleepThreshold; + rigidbodyCopy.freezeRotation = original.freezeRotation; + rigidbodyCopy.position = original.position; + rigidbodyCopy.rotation = original.rotation; + rigidbodyCopy.velocity = original.velocity; + + // destroy original + GameObject.Destroy(original); + } + + // helper function: if a collider is on a child, copy that child first. + // this way child's relative position/rotation/scale are preserved. + public static GameObject CopyRelativeTransform(GameObject source, Transform sourceChild, GameObject destination) + { + // is this on the source root? then we want to put it on the destination root. + if (sourceChild == source.transform) return destination; + + // is this on a child? then create the same child with the same transform on destination. + // note this is technically only correct for the immediate child since + // .localPosition is relative to parent, but this is good enough. + GameObject child = new GameObject(sourceChild.name); + child.transform.SetParent(destination.transform, true); + child.transform.localPosition = sourceChild.localPosition; + child.transform.localRotation = sourceChild.localRotation; + child.transform.localScale = sourceChild.localScale; + + // assign the same Layer for the physics copy. + // games may use a custom physics collision matrix, layer matters. + child.layer = sourceChild.gameObject.layer; + + return child; + } + + // colliders /////////////////////////////////////////////////////////// + // move all BoxColliders + settings from one GameObject to another. + public static void MoveBoxColliders(GameObject source, GameObject destination) + { + // colliders may be on children + BoxCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (BoxCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + BoxCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.size = sourceCollider.size; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + GameObject.Destroy(sourceCollider); + } + } + + // move all SphereColliders + settings from one GameObject to another. + public static void MoveSphereColliders(GameObject source, GameObject destination) + { + // colliders may be on children + SphereCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (SphereCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + SphereCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + GameObject.Destroy(sourceCollider); + } + } + + // move all CapsuleColliders + settings from one GameObject to another. + public static void MoveCapsuleColliders(GameObject source, GameObject destination) + { + // colliders may be on children + CapsuleCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (CapsuleCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + CapsuleCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.height = sourceCollider.height; + colliderCopy.direction = sourceCollider.direction; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + GameObject.Destroy(sourceCollider); + } + } + + // move all MeshColliders + settings from one GameObject to another. + public static void MoveMeshColliders(GameObject source, GameObject destination) + { + // colliders may be on children + MeshCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (MeshCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + MeshCollider colliderCopy = target.AddComponent(); + colliderCopy.sharedMesh = sourceCollider.sharedMesh; + colliderCopy.convex = sourceCollider.convex; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + GameObject.Destroy(sourceCollider); + } + } + + // move all Colliders + settings from one GameObject to another. + public static void MoveAllColliders(GameObject source, GameObject destination) + { + MoveBoxColliders(source, destination); + MoveSphereColliders(source, destination); + MoveCapsuleColliders(source, destination); + MoveMeshColliders(source, destination); + } + + // joints ////////////////////////////////////////////////////////////// + // move all CharacterJoints + settings from one GameObject to another. + public static void MoveCharacterJoints(GameObject source, GameObject destination) + { + // colliders may be on children + CharacterJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (CharacterJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + CharacterJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.enableProjection = sourceJoint.enableProjection; + jointCopy.highTwistLimit = sourceJoint.highTwistLimit; + jointCopy.lowTwistLimit = sourceJoint.lowTwistLimit; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.projectionAngle = sourceJoint.projectionAngle; + jointCopy.projectionDistance = sourceJoint.projectionDistance; + jointCopy.swing1Limit = sourceJoint.swing1Limit; + jointCopy.swing2Limit = sourceJoint.swing2Limit; + jointCopy.swingAxis = sourceJoint.swingAxis; + jointCopy.swingLimitSpring = sourceJoint.swingLimitSpring; + jointCopy.twistLimitSpring = sourceJoint.twistLimitSpring; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + GameObject.Destroy(sourceJoint); + } + } + + // move all ConfigurableJoints + settings from one GameObject to another. + public static void MoveConfigurableJoints(GameObject source, GameObject destination) + { + // colliders may be on children + ConfigurableJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (ConfigurableJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + ConfigurableJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.angularXLimitSpring = sourceJoint.angularXLimitSpring; + jointCopy.angularXDrive = sourceJoint.angularXDrive; + jointCopy.angularXMotion = sourceJoint.angularXMotion; + jointCopy.angularYLimit = sourceJoint.angularYLimit; + jointCopy.angularYMotion = sourceJoint.angularYMotion; + jointCopy.angularYZDrive = sourceJoint.angularYZDrive; + jointCopy.angularYZLimitSpring = sourceJoint.angularYZLimitSpring; + jointCopy.angularZLimit = sourceJoint.angularZLimit; + jointCopy.angularZMotion = sourceJoint.angularZMotion; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.configuredInWorldSpace = sourceJoint.configuredInWorldSpace; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; + jointCopy.linearLimitSpring = sourceJoint.linearLimitSpring; + jointCopy.linearLimit = sourceJoint.linearLimit; + jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.projectionAngle = sourceJoint.projectionAngle; + jointCopy.projectionDistance = sourceJoint.projectionDistance; + jointCopy.projectionMode = sourceJoint.projectionMode; + jointCopy.rotationDriveMode = sourceJoint.rotationDriveMode; + jointCopy.secondaryAxis = sourceJoint.secondaryAxis; + jointCopy.slerpDrive = sourceJoint.slerpDrive; + jointCopy.swapBodies = sourceJoint.swapBodies; + jointCopy.targetAngularVelocity = sourceJoint.targetAngularVelocity; + jointCopy.targetPosition = sourceJoint.targetPosition; + jointCopy.targetRotation = sourceJoint.targetRotation; + jointCopy.targetVelocity = sourceJoint.targetVelocity; + jointCopy.xDrive = sourceJoint.xDrive; + jointCopy.xMotion = sourceJoint.xMotion; + jointCopy.yDrive = sourceJoint.yDrive; + jointCopy.yMotion = sourceJoint.yMotion; + jointCopy.zDrive = sourceJoint.zDrive; + jointCopy.zMotion = sourceJoint.zMotion; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + GameObject.Destroy(sourceJoint); + } + } + + // move all FixedJoints + settings from one GameObject to another. + public static void MoveFixedJoints(GameObject source, GameObject destination) + { + // colliders may be on children + FixedJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (FixedJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + FixedJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.massScale = sourceJoint.massScale; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + GameObject.Destroy(sourceJoint); + } + } + + // move all HingeJoints + settings from one GameObject to another. + public static void MoveHingeJoints(GameObject source, GameObject destination) + { + // colliders may be on children + HingeJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (HingeJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + HingeJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.limits = sourceJoint.limits; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.motor = sourceJoint.motor; + jointCopy.spring = sourceJoint.spring; + jointCopy.useLimits = sourceJoint.useLimits; + jointCopy.useMotor = sourceJoint.useMotor; + jointCopy.useSpring = sourceJoint.useSpring; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif +#if UNITY_2022_3_OR_NEWER + jointCopy.extendedLimits = sourceJoint.extendedLimits; + jointCopy.useAcceleration = sourceJoint.useAcceleration; +#endif + + GameObject.Destroy(sourceJoint); + } + } + + // move all SpringJoints + settings from one GameObject to another. + public static void MoveSpringJoints(GameObject source, GameObject destination) + { + // colliders may be on children + SpringJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (SpringJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + SpringJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.damper = sourceJoint.damper; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.maxDistance = sourceJoint.maxDistance; + jointCopy.minDistance = sourceJoint.minDistance; + jointCopy.spring = sourceJoint.spring; + jointCopy.tolerance = sourceJoint.tolerance; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + GameObject.Destroy(sourceJoint); + } + } + + // move all Joints + settings from one GameObject to another. + public static void MoveAllJoints(GameObject source, GameObject destination) + { + MoveCharacterJoints(source, destination); + MoveConfigurableJoints(source, destination); + MoveFixedJoints(source, destination); + MoveHingeJoints(source, destination); + MoveSpringJoints(source, destination); + } + + // all ///////////////////////////////////////////////////////////////// + // move all physics components from one GameObject to another. + public static void MovePhysicsComponents(GameObject source, GameObject destination) + { + // need to move joints first, otherwise we might see: + // 'can't move Rigidbody because a Joint depends on it' + MoveAllJoints(source, destination); + MoveAllColliders(source, destination); + MoveRigidbody(source, destination); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta new file mode 100644 index 000000000..52cc7375c --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17cfe1beb3f94a69b94bf60afc37ef7a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Core/InterestManagement.cs b/Assets/Mirror/Core/InterestManagement.cs index 3516339ef..88ead0ffb 100644 --- a/Assets/Mirror/Core/InterestManagement.cs +++ b/Assets/Mirror/Core/InterestManagement.cs @@ -54,7 +54,7 @@ public override void Rebuild(NetworkIdentity identity, bool initialize) newObservers.Clear(); // not force hidden? - if (identity.visible != Visibility.ForceHidden) + if (identity.visibility != Visibility.ForceHidden) { OnRebuildObservers(identity, newObservers); } diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index 45a3e0809..9240d7ffc 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -312,13 +312,13 @@ protected virtual void OnValidate() // GetComponentInParent(includeInactive) is needed because Prefabs are not // considered active, so this check requires to scan inactive. #if UNITY_EDITOR -#if UNITY_2021_3_OR_NEWER // 2021 has GetComponentInParents(active) +#if UNITY_2021_3_OR_NEWER // 2021 has GetComponentInParent(bool includeInactive = false) if (GetComponent() == null && GetComponentInParent(true) == null) { Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this); } -#elif UNITY_2020_3_OR_NEWER // 2020 only has GetComponentsInParents(active), we can use this too +#elif UNITY_2020_3_OR_NEWER // 2020 only has GetComponentsInParent(bool includeInactive = false), we can use this too NetworkIdentity[] parentsIds = GetComponentsInParent(true); int parentIdsCount = parentsIds != null ? parentsIds.Length : 0; if (GetComponent() == null && parentIdsCount == 0) diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index e1362c1a5..479210877 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -561,6 +561,10 @@ public static void ReplaceHandler(Action handler, bool // 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; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + void HandlerWrapped(NetworkConnection _, T value) => handler(_, value); handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } @@ -575,6 +579,10 @@ public static void ReplaceHandler(Action handler, bool requireAuthenticati // 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; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + void HandlerWrapped(NetworkConnection _, T value) => handler(value); handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } @@ -589,6 +597,10 @@ public static void ReplaceHandler(Action handler, bool requireAuthent // 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; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId); handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs index 331ab3e44..1f9cef182 100644 --- a/Assets/Mirror/Core/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -197,10 +197,18 @@ internal set // ForceHidden = useful to hide monsters while they respawn etc. // ForceShown = useful to have score NetworkIdentities that always broadcast // to everyone etc. - // - // TODO rename to 'visibility' after removing .visibility some day! [Tooltip("Visibility can overwrite interest management. ForceHidden can be useful to hide monsters while they respawn. ForceShown can be useful for score NetworkIdentities that should always broadcast to everyone in the world.")] - public Visibility visible = Visibility.Default; + [FormerlySerializedAs("visible")] + public Visibility visibility = Visibility.Default; + + // Deprecated 2024-01-21 + [HideInInspector] + [Obsolete("Deprecated - Use .visibility instead. This will be removed soon.")] + public Visibility visible + { + get => visibility; + set => visibility = value; + } // broadcasting serializes all entities around a player for each player. // we don't want to serialize one entity twice in the same tick. diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 8cb713604..547ba470c 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -928,6 +928,10 @@ public static void ReplaceHandler(Action handle where T : struct, NetworkMessage { ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } @@ -936,6 +940,10 @@ public static void ReplaceHandler(Action h where T : struct, NetworkMessage { ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } @@ -1206,17 +1214,17 @@ static void SpawnObserversForConnection(NetworkConnectionToClient conn) // first! // ForceShown: add no matter what - if (identity.visible == Visibility.ForceShown) + if (identity.visibility == Visibility.ForceShown) { identity.AddObserver(conn); } // ForceHidden: don't show no matter what - else if (identity.visible == Visibility.ForceHidden) + else if (identity.visibility == Visibility.ForceHidden) { // do nothing } // default: legacy system / new system / no system support - else if (identity.visible == Visibility.Default) + else if (identity.visibility == Visibility.Default) { // aoi system if (aoi != null) @@ -1682,7 +1690,7 @@ static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) if (initialize) { // not force hidden? - if (identity.visible != Visibility.ForceHidden) + if (identity.visibility != Visibility.ForceHidden) { AddAllReadyServerConnectionsToObservers(identity); } @@ -1725,7 +1733,7 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize) { // if there is no interest management system, // or if 'force shown' then add all connections - if (aoi == null || identity.visible == Visibility.ForceShown) + if (aoi == null || identity.visibility == Visibility.ForceShown) { RebuildObserversDefault(identity, initialize); } diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs index 8119f0809..76e3ac7ed 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs @@ -471,13 +471,6 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, DictionarySlower, but more reliable; works in WebGL. TCP, + + /// Slower, but more reliable; works in WebGL. + WS, } } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs index c706d8c58..8a1d76b1c 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs @@ -61,7 +61,7 @@ public class UpdateAppVersionRequest #endregion // (!) Shows in API docs for PATCH, but could be CREATE only? "Unknown Args" [JsonProperty("max_duration")] - public int MaxDuration { get; set; } = 30; + public int MaxDuration { get; set; } = 60; [JsonProperty("use_telemetry")] public bool UseTelemetry { get; set; } = true; @@ -70,7 +70,7 @@ public class UpdateAppVersionRequest public bool InjectContextEnv { get; set; } = true; [JsonProperty("whitelisting_active")] - public bool WhitelistingActive { get; set; } = true; + public bool WhitelistingActive { get; set; } = false; [JsonProperty("force_cache")] public bool ForceCache { get; set; } @@ -82,7 +82,7 @@ public class UpdateAppVersionRequest public int CacheMaxHour { get; set; } [JsonProperty("time_to_deploy")] - public int TimeToDeploy { get; set; } = 15; + public int TimeToDeploy { get; set; } = 120; [JsonProperty("enable_all_locations")] public bool EnableAllLocations { get; set; } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs index b8ef1c4aa..821210ea5 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs @@ -237,10 +237,10 @@ private void setVisualElementsToFields() _containerRegistryFoldout = rootVisualElement.Q(EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID); _containerNewTagVersionInput = rootVisualElement.Q(EdgegapWindowMetadata.CONTAINER_NEW_TAG_VERSION_TXT_ID); _containerPortNumInput = rootVisualElement.Q(EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID); - // MIRROR CHANGE: dynamically resolving PortType fails if not in Assembly-CSharp-Editor.dll. Hardcode UDP/TCP instead. + // MIRROR CHANGE: dynamically resolving PortType fails if not in Assembly-CSharp-Editor.dll. Hardcode UDP/TCP/WS instead. // this finds the placeholder and dynamically replaces it with a popup field VisualElement dropdownPlaceholder = rootVisualElement.Q("MIRROR_CHANGE_PORT_HARDCODED"); - List options = new List { "UDP", "TCP" }; + List options = Enum.GetNames(typeof(ProtocolType)).Cast().ToList(); _containerTransportTypeEnumInput = new PopupField("Protocol Type", options, 0); dropdownPlaceholder.Add(_containerTransportTypeEnumInput); // END MIRROR CHANGE @@ -1606,6 +1606,7 @@ private async Task buildAndPushServerAsync() { Port = int.Parse(_containerPortNumInput.value), // OnInputChange clamps + validates, ProtocolStr = _containerTransportTypeEnumInput.value.ToString(), + TlsUpgrade = _containerTransportTypeEnumInput.value.ToString() == ProtocolType.WS.ToString() // If the protocol is WebSocket, we seemlessly add tls_upgrade. If we want to add it to other protocols, we need to change this. }, }; diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs.meta index ee4316428..4368f4287 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs.meta +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs.meta @@ -3,10 +3,7 @@ guid: 1c3d4497250ad3e4aa500d4c599b30fe MonoImporter: externalObjects: {} serializedVersion: 2 - defaultReferences: - - m_ViewDataDictionary: {instanceID: 0} - - LogoImage: {fileID: 2800000, guid: b7012da4ebf9008458abc3ef9a741f3c, type: 3} - - ClipboardImage: {fileID: 2800000, guid: caa516cdb721dd143bbc8000ca78d50a, type: 3} + defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: diff --git a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Default.cs b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Default.cs index 048277ef7..d158ded86 100644 --- a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Default.cs +++ b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Default.cs @@ -11,7 +11,7 @@ public class InterestManagementTests_Default : InterestManagementTests_Common public override void ForceHidden_Initial() { // force hide A - identityA.visible = Visibility.ForceHidden; + identityA.visibility = Visibility.ForceHidden; // rebuild for both // initial rebuild adds all connections if no interest management available @@ -30,7 +30,7 @@ public override void ForceHidden_Initial() public override void ForceShown_Initial() { // force show A - identityA.visible = Visibility.ForceShown; + identityA.visibility = Visibility.ForceShown; // rebuild for both // initial rebuild adds all connections if no interest management available diff --git a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Distance.cs b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Distance.cs index 17276f554..5f25010a2 100644 --- a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Distance.cs +++ b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_Distance.cs @@ -36,7 +36,7 @@ public override void ForceHidden_Initial() // A and B are at (0,0,0) so within range! // force hide A - identityA.visible = Visibility.ForceHidden; + identityA.visibility = Visibility.ForceHidden; // rebuild for both // initial rebuild while both are within range @@ -58,7 +58,7 @@ public override void ForceShown_Initial() identityB.transform.position = Vector3.right * (aoi.visRange + 1); // force show A - identityA.visible = Visibility.ForceShown; + identityA.visibility = Visibility.ForceShown; // rebuild for both // initial rebuild while both are within range diff --git a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs index 8fdc13458..9a708ed21 100644 --- a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs +++ b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs @@ -39,7 +39,7 @@ public override void ForceHidden_Initial() // A and B are at (0,0,0) so within range! // force hide A - identityA.visible = Visibility.ForceHidden; + identityA.visibility = Visibility.ForceHidden; // rebuild for both // initial rebuild while both are within range @@ -61,7 +61,7 @@ public override void ForceShown_Initial() identityB.transform.position = Vector3.right * (aoi.visRange + 1); // force show A - identityA.visible = Visibility.ForceShown; + identityA.visibility = Visibility.ForceShown; // update grid now that positions were changed aoi.Update(); diff --git a/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests.cs b/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests.cs index 08fa8c03a..a02fe69f9 100644 --- a/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests.cs +++ b/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests.cs @@ -38,13 +38,6 @@ public void SyncVarsUnityComponent() "UnityEngine.TextMesh WeaverSyncVarTests.SyncVarsUnityComponent.SyncVarsUnityComponent::invalidVar"); } - [Test] - public void SyncVarsCantBeArray() - { - HasError("thisShouldntWork has invalid type. Use SyncLists instead of arrays", - "System.Int32[] WeaverSyncVarTests.SyncVarsCantBeArray.SyncVarsCantBeArray::thisShouldntWork"); - } - [Test] public void SyncVarsSyncList() { diff --git a/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests~/SyncVarsCantBeArray.cs b/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests_IsSuccess/SyncVarsCanBeArray.cs similarity index 50% rename from Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests~/SyncVarsCantBeArray.cs rename to Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests_IsSuccess/SyncVarsCanBeArray.cs index 7f346da6f..786669e58 100644 --- a/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests~/SyncVarsCantBeArray.cs +++ b/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests_IsSuccess/SyncVarsCanBeArray.cs @@ -2,9 +2,9 @@ namespace WeaverSyncVarTests.SyncVarsCantBeArray { - class SyncVarsCantBeArray : NetworkBehaviour + class SyncVarsCanBeArray : NetworkBehaviour { [SyncVar] - int[] thisShouldntWork = new int[100]; + int[] thisShouldWork = new int[100]; } } diff --git a/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests_IsSuccess/SyncVarsCanBeArray.cs.meta b/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests_IsSuccess/SyncVarsCanBeArray.cs.meta new file mode 100644 index 000000000..bf8af1e03 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Weaver/WeaverSyncVarAttributeTests_IsSuccess/SyncVarsCanBeArray.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d0708030f93e2f498530168e9c644c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index a42609d3a..95b9a48e4 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ Many of our features quickly became the norm across all Unity netcodes!
| ☁️ **Two Click Hosting** | (Optional) Build & Push directly from Unity Editor to the Cloud. | **Preview** | | | | | | 📏 **Snapshot Interp.** | Perfectly smooth movement for all platforms and all games. | **Stable** | +| 🏎 **Fast Prediction** | Simulate Physics locally & apply server corrections **[VR ready]** | **Beta** | | 🔫 **Lag Compensation** | Roll back state to see what the player saw during input. | **Preview** | -| 🏎 **Prediction** | Inputs are applied immediately & corrected automatically. | **Preview** | | | | | | 🧙‍♂️ **General Purpose** | Mirror supports all genres for all your games! | | | 🧘‍♀️ **Stable API** | Long term (10 years) stability instead of new versions! |