Merged master

This commit is contained in:
MrGadget 2024-01-29 02:14:35 -05:00
commit 7821e24789
28 changed files with 717 additions and 261 deletions

View File

@ -14,8 +14,8 @@ jobs:
- 2019.4.40f1 - 2019.4.40f1
- 2020.3.48f1 - 2020.3.48f1
- 2021.3.33f1 - 2021.3.33f1
- 2022.3.16f1 - 2022.3.18f1
- 2023.2.4f1 - 2023.2.7f1
steps: steps:
- name: Checkout repository - name: Checkout repository
@ -47,7 +47,7 @@ jobs:
customParameters: -stackTraceLogType None customParameters: -stackTraceLogType None
- name: Archive test results - name: Archive test results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: Test Results ${{ matrix.unityVersion }} name: Test Results ${{ matrix.unityVersion }}

View File

@ -11,7 +11,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -32,7 +32,7 @@ jobs:
- name: Package - name: Package
run: unity-packer pack Mirror.unitypackage Assets/Mirror Assets/Mirror Assets/ScriptTemplates Assets/ScriptTemplates LICENSE Assets/Mirror/LICENSE 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: with:
name: Mirror.unitypackage name: Mirror.unitypackage
path: Mirror.unitypackage path: Mirror.unitypackage

View File

@ -1,4 +1,5 @@
// simple component that holds team information // simple component that holds team information
using System;
using UnityEngine; using UnityEngine;
namespace Mirror namespace Mirror
@ -8,10 +9,31 @@ namespace Mirror
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
public class NetworkTeam : NetworkBehaviour public class NetworkTeam : NetworkBehaviour
{ {
[Tooltip("Set this to the same value on all networked objects that belong to a given team")] [SerializeField]
[SyncVar] public string teamId = string.Empty; [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")] [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;
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace Mirror namespace Mirror
@ -6,126 +6,112 @@ namespace Mirror
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")] [AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
public class TeamInterestManagement : InterestManagement public class TeamInterestManagement : InterestManagement
{ {
readonly Dictionary<string, HashSet<NetworkIdentity>> teamObjects = new Dictionary<string, HashSet<NetworkIdentity>>(); readonly Dictionary<string, HashSet<NetworkTeam>> teamObjects =
readonly Dictionary<NetworkIdentity, string> lastObjectTeam = new Dictionary<NetworkIdentity, string>(); new Dictionary<string, HashSet<NetworkTeam>>();
readonly HashSet<string> dirtyTeams = new HashSet<string>(); readonly HashSet<string> dirtyTeams = new HashSet<string>();
// 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<NetworkTeam>();
// Add this object to the hashset of the new team
teamObjects[networkTeam.teamId].Add(networkTeam);
}
[ServerCallback] [ServerCallback]
public override void OnSpawned(NetworkIdentity identity) public override void OnSpawned(NetworkIdentity identity)
{ {
if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) if (!identity.TryGetComponent(out NetworkTeam networkTeam))
return; return;
string networkTeamId = identityNetworkTeam.teamId; string networkTeamId = networkTeam.teamId;
lastObjectTeam[identity] = networkTeamId;
// Null / Empty string is never a valid teamId...do not add to teamObjects collection // Null / Empty string is never a valid teamId...do not add to teamObjects collection
if (string.IsNullOrWhiteSpace(networkTeamId)) if (string.IsNullOrWhiteSpace(networkTeamId))
return; return;
//Debug.Log($"TeamInterestManagement.OnSpawned {identity.name} {networkTeamId}"); // Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}");
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkTeam> objects))
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkIdentity> objects))
{ {
objects = new HashSet<NetworkIdentity>(); objects = new HashSet<NetworkTeam>();
teamObjects.Add(networkTeamId, objects); teamObjects.Add(networkTeamId, objects);
} }
objects.Add(identity); objects.Add(networkTeam);
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object. // 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. // 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); dirtyTeams.Add(networkTeamId);
} }
[ServerCallback] [ServerCallback]
public override void OnDestroyed(NetworkIdentity identity) 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 // Multiple objects could be destroyed in same frame and we don't
// want to rebuild for each one...let Update do it once. // want to rebuild for each one...let LateUpdate do it once.
// We must add the current team to dirtyTeams for Update to rebuild it. // We must add the current team to dirtyTeames for LateUpdate to rebuild it.
if (lastObjectTeam.TryGetValue(identity, out string currentTeam)) if (identity.TryGetComponent(out NetworkTeam currentTeam))
{ {
lastObjectTeam.Remove(identity); if (!string.IsNullOrWhiteSpace(currentTeam.teamId) &&
if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet<NetworkIdentity> objects) && objects.Remove(identity)) teamObjects.TryGetValue(currentTeam.teamId, out HashSet<NetworkTeam> objects) &&
dirtyTeams.Add(currentTeam); 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<NetworkIdentity>());
// 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) public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
{ {
// Always observed if no NetworkTeam component // Always observed if no NetworkTeam component
@ -135,7 +121,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
if (identityNetworkTeam.forceShown) if (identityNetworkTeam.forceShown)
return true; return true;
// string.Empty is never a valid teamId // Null / Empty string is never a valid teamId
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId)) if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
return false; 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}"); //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; return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
} }
@ -173,14 +159,14 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
if (string.IsNullOrWhiteSpace(networkTeam.teamId)) if (string.IsNullOrWhiteSpace(networkTeam.teamId))
return; return;
// Abort if this team hasn't been created yet by OnSpawned or UpdateTeamObjects // Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkIdentity> objects)) if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkTeam> objects))
return; return;
// Add everything in the hashset for this object's current team // Add everything in the hashset for this object's current team
foreach (NetworkIdentity networkIdentity in objects) foreach (NetworkTeam netTeam in objects)
if (networkIdentity != null && networkIdentity.connectionToClient != null) if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null)
newObservers.Add(networkIdentity.connectionToClient); newObservers.Add(netTeam.netIdentity.connectionToClient);
} }
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers) void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)

View File

@ -23,6 +23,7 @@ public enum CorrectionMode
// [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it.
public class PredictedRigidbody : NetworkBehaviour 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. Rigidbody rb; // own rigidbody on server. this is never moved to a physics copy.
Vector3 lastPosition; Vector3 lastPosition;
@ -79,87 +80,19 @@ public class PredictedRigidbody : NetworkBehaviour
// Rigidbody & Collider are moved out into a separate object. // Rigidbody & Collider are moved out into a separate object.
// this way the visual object can smoothly follow. // this way the visual object can smoothly follow.
protected GameObject physicsCopy; protected GameObject physicsCopy;
Transform physicsCopyTransform; // caching to avoid GetComponent
Rigidbody physicsCopyRigidbody; // 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. // we also create one extra ghost for the exact known server state.
protected GameObject remoteCopy; protected GameObject remoteCopy;
void Awake() void Awake()
{ {
tf = transform;
rb = GetComponent<Rigidbody>(); rb = GetComponent<Rigidbody>();
} if (rb == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component.");
protected virtual Rigidbody MoveRigidbody(GameObject destination)
{
Rigidbody source = GetComponent<Rigidbody>();
Rigidbody rigidbodyCopy = destination.AddComponent<Rigidbody>();
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<BoxCollider>();
foreach (BoxCollider sourceCollider in sourceColliders)
{
BoxCollider colliderCopy = destination.AddComponent<BoxCollider>();
colliderCopy.center = sourceCollider.center;
colliderCopy.size = sourceCollider.size;
Destroy(sourceCollider);
}
}
protected virtual void MoveSphereColliders(GameObject destination)
{
SphereCollider[] sourceColliders = GetComponents<SphereCollider>();
foreach (SphereCollider sourceCollider in sourceColliders)
{
SphereCollider colliderCopy = destination.AddComponent<SphereCollider>();
colliderCopy.center = sourceCollider.center;
colliderCopy.radius = sourceCollider.radius;
Destroy(sourceCollider);
}
}
protected virtual void MoveCapsuleColliders(GameObject destination)
{
CapsuleCollider[] sourceColliders = GetComponents<CapsuleCollider>();
foreach (CapsuleCollider sourceCollider in sourceColliders)
{
CapsuleCollider colliderCopy = destination.AddComponent<CapsuleCollider>();
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<MeshCollider>();
foreach (MeshCollider sourceCollider in sourceColliders)
{
MeshCollider colliderCopy = destination.AddComponent<MeshCollider>();
colliderCopy.sharedMesh = sourceCollider.sharedMesh;
colliderCopy.convex = sourceCollider.convex;
colliderCopy.isTrigger = sourceCollider.isTrigger;
Destroy(sourceCollider);
}
} }
protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) protected virtual void CopyRenderersAsGhost(GameObject destination, Material material)
@ -203,25 +136,32 @@ protected virtual void CreateGhosts()
Debug.Log($"Separating Physics for {name}"); Debug.Log($"Separating Physics for {name}");
// create an empty GameObject with the same name + _Physical // 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 = new GameObject($"{name}_Physical");
physicsCopy.transform.position = transform.position; physicsCopy.transform.position = tf.position; // world position!
physicsCopy.transform.rotation = transform.rotation; physicsCopy.transform.rotation = tf.rotation; // world rotation!
physicsCopy.transform.localScale = transform.localScale; 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 // add the PredictedRigidbodyPhysical component
PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent<PredictedRigidbodyPhysicsGhost>(); PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent<PredictedRigidbodyPhysicsGhost>();
physicsGhostRigidbody.target = this; physicsGhostRigidbody.target = tf;
physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold;
physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval;
// move the rigidbody component to the physics GameObject // move the rigidbody component & all colliders to the physics GameObject
MoveRigidbody(physicsCopy); PredictionUtils.MovePhysicsComponents(gameObject, 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 // show ghost by copying all renderers / materials with ghost material applied
if (showGhost) if (showGhost)
@ -232,41 +172,70 @@ protected virtual void CreateGhosts()
physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval;
// one for the latest remote state for comparison // 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 = 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<PredictedRigidbodyRemoteGhost>(); PredictedRigidbodyRemoteGhost predictedGhost = remoteCopy.AddComponent<PredictedRigidbodyRemoteGhost>();
predictedGhost.target = this; predictedGhost.target = tf;
predictedGhost.ghostDistanceThreshold = ghostDistanceThreshold; predictedGhost.ghostDistanceThreshold = ghostDistanceThreshold;
predictedGhost.ghostEnabledCheckInterval = ghostEnabledCheckInterval; predictedGhost.ghostEnabledCheckInterval = ghostEnabledCheckInterval;
CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial); CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial);
} }
// cache components to avoid GetComponent calls at runtime // cache components to avoid GetComponent calls at runtime
physicsCopyTransform = physicsCopy.transform;
physicsCopyRigidbody = physicsCopy.GetComponent<Rigidbody>(); physicsCopyRigidbody = physicsCopy.GetComponent<Rigidbody>();
physicsCopyCollider = physicsCopy.GetComponent<Collider>(); physicsCopyCollider = physicsCopy.GetComponentInChildren<Collider>();
if (physicsGhostRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); if (physicsCopyRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody.");
if (physicsCopyCollider == null) throw new Exception("SeparatePhysics: couldn't find final Collider."); 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); if (remoteCopy != null) Destroy(remoteCopy);
} }
// this shows in profiler LateUpdates! need to make this as fast as possible!
protected virtual void SmoothFollowPhysicsCopy() protected virtual void SmoothFollowPhysicsCopy()
{ {
// hard follow: // hard follow:
// transform.position = physicsCopyCollider.position; // tf.position = physicsCopyCollider.position;
// transform.rotation = physicsCopyCollider.rotation; // tf.rotation = physicsCopyCollider.rotation;
// ORIGINAL VERSION: CLEAN AND SIMPLE
/*
// if we are further than N colliders sizes behind, then teleport // if we are further than N colliders sizes behind, then teleport
float colliderSize = physicsCopyCollider.bounds.size.magnitude; float colliderSize = physicsCopyCollider.bounds.size.magnitude;
float threshold = colliderSize * teleportDistanceMultiplier; float threshold = colliderSize * teleportDistanceMultiplier;
float distance = Vector3.Distance(transform.position, physicsCopyRigidbody.position); float distance = Vector3.Distance(tf.position, physicsCopyRigidbody.position);
if (distance > threshold) if (distance > threshold)
{ {
transform.position = physicsCopyRigidbody.position; tf.position = physicsCopyRigidbody.position;
transform.rotation = physicsCopyRigidbody.rotation; tf.rotation = physicsCopyRigidbody.rotation;
Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}"); Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}");
return; return;
} }
@ -274,15 +243,42 @@ protected virtual void SmoothFollowPhysicsCopy()
// smoothly interpolate to the target position. // smoothly interpolate to the target position.
// speed relative to how far away we are // speed relative to how far away we are
float positionStep = distance * positionInterpolationSpeed; 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 relative to how far away we are.
// => speed increases by distance² because the further away, the // => speed increases by distance² because the further away, the
// sooner we need to catch the fuck up // sooner we need to catch the fuck up
// float positionStep = (distance * distance) * interpolationSpeed; // 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. // smoothly interpolate to the target rotation.
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. // 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. // creater visual copy only on clients, where players are watching.
@ -294,14 +290,17 @@ public override void OnStartClient()
// destroy visual copy only in OnStopClient(). // destroy visual copy only in OnStopClient().
// OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed. // 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() void UpdateServer()
{ {
// to save bandwidth, we only serialize when position changed // 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(); // 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. // this is fine because the visual object still smoothly interpolates to it.
if (physicsCopyRigidbody.velocity.magnitude <= snapThreshold) 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. // apply server state immediately.
// important to apply velocity as well, instead of Vector3.zero. // important to apply velocity as well, instead of Vector3.zero.
// in case an object is still slightly moving, we don't want it // 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.position = state.position;
remoteCopy.transform.rotation = state.rotation; 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. // 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. // otherwise it could be out of sync as long as it's too far behind.
if (state.timestamp < oldest.timestamp) 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); ApplyState(state.timestamp, state.position, state.rotation, state.velocity);
return; return;
} }

View File

@ -7,8 +7,10 @@ namespace Mirror
{ {
public class PredictedRigidbodyPhysicsGhost : MonoBehaviour 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.")] [Tooltip("The predicted rigidbody owner.")]
public PredictedRigidbody target; public Transform target;
// ghost (settings are copyed from PredictedRigidbody) // ghost (settings are copyed from PredictedRigidbody)
MeshRenderer ghost; MeshRenderer ghost;
@ -16,13 +18,15 @@ public class PredictedRigidbodyPhysicsGhost : MonoBehaviour
public float ghostEnabledCheckInterval = 0.2f; public float ghostEnabledCheckInterval = 0.2f;
double lastGhostEnabledCheckTime = 0; double lastGhostEnabledCheckTime = 0;
// cache // cache components because this is performance critical!
Transform tf;
Collider co; Collider co;
// we add this component manually from PredictedRigidbody. // we add this component manually from PredictedRigidbody.
// so assign this in Start. target isn't set in Awake yet. // so assign this in Start. target isn't set in Awake yet.
void Start() void Start()
{ {
tf = transform;
co = GetComponent<Collider>(); co = GetComponent<Collider>();
ghost = GetComponent<MeshRenderer>(); ghost = GetComponent<MeshRenderer>();
} }
@ -43,7 +47,7 @@ void UpdateGhostRenderers()
// otherwise it just looks like z-fighting the whole time. // otherwise it just looks like z-fighting the whole time.
// => iterated the renderers we found when creating the visual copy. // => iterated the renderers we found when creating the visual copy.
// we don't want to GetComponentsInChildren every time here! // 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; ghost.enabled = !insideTarget;
} }
@ -53,7 +57,7 @@ void UpdateGhostRenderers()
void LateUpdate() void LateUpdate()
{ {
// if owner gets network destroyed for any reason, destroy visual // 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. // also show a yellow gizmo for the predicted & corrected physics.

View File

@ -5,8 +5,10 @@ namespace Mirror
{ {
public class PredictedRigidbodyRemoteGhost : MonoBehaviour 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.")] [Tooltip("The predicted rigidbody owner.")]
public PredictedRigidbody target; public Transform target;
// ghost (settings are copyed from PredictedRigidbody) // ghost (settings are copyed from PredictedRigidbody)
MeshRenderer ghost; MeshRenderer ghost;
@ -14,10 +16,14 @@ public class PredictedRigidbodyRemoteGhost : MonoBehaviour
public float ghostEnabledCheckInterval = 0.2f; public float ghostEnabledCheckInterval = 0.2f;
double lastGhostEnabledCheckTime = 0; double lastGhostEnabledCheckTime = 0;
// cache components because this is performance critical!
Transform tf;
// we add this component manually from PredictedRigidbody. // we add this component manually from PredictedRigidbody.
// so assign this in Start. target isn't set in Awake yet. // so assign this in Start. target isn't set in Awake yet.
void Start() void Start()
{ {
tf = transform;
ghost = GetComponent<MeshRenderer>(); ghost = GetComponent<MeshRenderer>();
} }
@ -37,7 +43,7 @@ void UpdateGhostRenderers()
// otherwise it just looks like z-fighting the whole time. // otherwise it just looks like z-fighting the whole time.
// => iterated the renderers we found when creating the visual copy. // => iterated the renderers we found when creating the visual copy.
// we don't want to GetComponentsInChildren every time here! // 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; ghost.enabled = !insideTarget;
} }
@ -47,7 +53,7 @@ void UpdateGhostRenderers()
void LateUpdate() void LateUpdate()
{ {
// if owner gets network destroyed for any reason, destroy visual // if owner gets network destroyed for any reason, destroy visual
if (target == null || target.gameObject == null) Destroy(gameObject); if (target == null) Destroy(gameObject);
} }
} }
} }

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 62e7e9424c7e48d69b6a3517796142a1 guid: 62e7e9424c7e48d69b6a3517796142a1
timeCreated: 1705235542 MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<Rigidbody>();
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<Rigidbody>();
// 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<BoxCollider>();
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<BoxCollider>();
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<SphereCollider>();
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<SphereCollider>();
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<CapsuleCollider>();
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<CapsuleCollider>();
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<MeshCollider>();
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<MeshCollider>();
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<CharacterJoint>();
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<CharacterJoint>();
// 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<ConfigurableJoint>();
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<ConfigurableJoint>();
// 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<FixedJoint>();
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<FixedJoint>();
// 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<HingeJoint>();
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<HingeJoint>();
// 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<SpringJoint>();
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<SpringJoint>();
// 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);
}
}
}

View File

@ -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:

View File

@ -54,7 +54,7 @@ public override void Rebuild(NetworkIdentity identity, bool initialize)
newObservers.Clear(); newObservers.Clear();
// not force hidden? // not force hidden?
if (identity.visible != Visibility.ForceHidden) if (identity.visibility != Visibility.ForceHidden)
{ {
OnRebuildObservers(identity, newObservers); OnRebuildObservers(identity, newObservers);
} }

View File

@ -312,13 +312,13 @@ protected virtual void OnValidate()
// GetComponentInParent(includeInactive) is needed because Prefabs are not // GetComponentInParent(includeInactive) is needed because Prefabs are not
// considered active, so this check requires to scan inactive. // considered active, so this check requires to scan inactive.
#if UNITY_EDITOR #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<NetworkIdentity>() == null && if (GetComponent<NetworkIdentity>() == null &&
GetComponentInParent<NetworkIdentity>(true) == null) GetComponentInParent<NetworkIdentity>(true) == null)
{ {
Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this); 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<NetworkIdentity>(true); NetworkIdentity[] parentsIds = GetComponentsInParent<NetworkIdentity>(true);
int parentIdsCount = parentsIds != null ? parentsIds.Length : 0; int parentIdsCount = parentsIds != null ? parentsIds.Length : 0;
if (GetComponent<NetworkIdentity>() == null && parentIdsCount == 0) if (GetComponent<NetworkIdentity>() == null && parentIdsCount == 0)

View File

@ -561,6 +561,10 @@ public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool
// so let's wrap it to ignore the NetworkConnection parameter. // so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection. // it's not needed on client. it's always NetworkClient.connection.
ushort msgType = NetworkMessageId<T>.Id; ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
void HandlerWrapped(NetworkConnection _, T value) => handler(_, value); void HandlerWrapped(NetworkConnection _, T value) => handler(_, value);
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect); handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
} }
@ -575,6 +579,10 @@ public static void ReplaceHandler<T>(Action<T> handler, bool requireAuthenticati
// so let's wrap it to ignore the NetworkConnection parameter. // so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection. // it's not needed on client. it's always NetworkClient.connection.
ushort msgType = NetworkMessageId<T>.Id; ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
void HandlerWrapped(NetworkConnection _, T value) => handler(value); void HandlerWrapped(NetworkConnection _, T value) => handler(value);
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect); handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
} }
@ -589,6 +597,10 @@ public static void ReplaceHandler<T>(Action<T, int> handler, bool requireAuthent
// so let's wrap it to ignore the NetworkConnection parameter. // so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection. // it's not needed on client. it's always NetworkClient.connection.
ushort msgType = NetworkMessageId<T>.Id; ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId); void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId);
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T, int>)HandlerWrapped, requireAuthentication, exceptionsDisconnect); handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T, int>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
} }

View File

@ -197,10 +197,18 @@ internal set
// ForceHidden = useful to hide monsters while they respawn etc. // ForceHidden = useful to hide monsters while they respawn etc.
// ForceShown = useful to have score NetworkIdentities that always broadcast // ForceShown = useful to have score NetworkIdentities that always broadcast
// to everyone etc. // 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.")] [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. // broadcasting serializes all entities around a player for each player.
// we don't want to serialize one entity twice in the same tick. // we don't want to serialize one entity twice in the same tick.

View File

@ -928,6 +928,10 @@ public static void ReplaceHandler<T>(Action<NetworkConnectionToClient, T> handle
where T : struct, NetworkMessage where T : struct, NetworkMessage
{ {
ushort msgType = NetworkMessageId<T>.Id; ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
} }
@ -936,6 +940,10 @@ public static void ReplaceHandler<T>(Action<NetworkConnectionToClient, T, int> h
where T : struct, NetworkMessage where T : struct, NetworkMessage
{ {
ushort msgType = NetworkMessageId<T>.Id; ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
} }
@ -1206,17 +1214,17 @@ static void SpawnObserversForConnection(NetworkConnectionToClient conn)
// first! // first!
// ForceShown: add no matter what // ForceShown: add no matter what
if (identity.visible == Visibility.ForceShown) if (identity.visibility == Visibility.ForceShown)
{ {
identity.AddObserver(conn); identity.AddObserver(conn);
} }
// ForceHidden: don't show no matter what // ForceHidden: don't show no matter what
else if (identity.visible == Visibility.ForceHidden) else if (identity.visibility == Visibility.ForceHidden)
{ {
// do nothing // do nothing
} }
// default: legacy system / new system / no system support // default: legacy system / new system / no system support
else if (identity.visible == Visibility.Default) else if (identity.visibility == Visibility.Default)
{ {
// aoi system // aoi system
if (aoi != null) if (aoi != null)
@ -1682,7 +1690,7 @@ static void RebuildObserversDefault(NetworkIdentity identity, bool initialize)
if (initialize) if (initialize)
{ {
// not force hidden? // not force hidden?
if (identity.visible != Visibility.ForceHidden) if (identity.visibility != Visibility.ForceHidden)
{ {
AddAllReadyServerConnectionsToObservers(identity); AddAllReadyServerConnectionsToObservers(identity);
} }
@ -1725,7 +1733,7 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize)
{ {
// if there is no interest management system, // if there is no interest management system,
// or if 'force shown' then add all connections // 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); RebuildObserversDefault(identity, initialize);
} }

View File

@ -471,13 +471,6 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<Fie
continue; continue;
} }
if (fd.FieldType.IsArray)
{
Log.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd);
WeavingFailed = true;
continue;
}
if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType)) if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType))
{ {
Log.Warning($"{fd.Name} has [SyncVar] attribute. SyncLists should not be marked with SyncVar", fd); Log.Warning($"{fd.Name} has [SyncVar] attribute. SyncLists should not be marked with SyncVar", fd);

View File

@ -366,7 +366,6 @@ MonoBehaviour:
deliveryTimeEmaDuration: 2 deliveryTimeEmaDuration: 2
connectionQualityInterval: 3 connectionQualityInterval: 3
timeInterpolationGui: 0 timeInterpolationGui: 0
spawnAmount: 50000
spawnAmount: 10000 spawnAmount: 10000
interleave: 2 interleave: 2
spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344, spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344,

View File

@ -11,5 +11,8 @@ public enum ProtocolType
/// <summary>Slower, but more reliable; works in WebGL.</summary> /// <summary>Slower, but more reliable; works in WebGL.</summary>
TCP, TCP,
/// <summary>Slower, but more reliable; works in WebGL.</summary>
WS,
} }
} }

View File

@ -61,7 +61,7 @@ public class UpdateAppVersionRequest
#endregion // (!) Shows in API docs for PATCH, but could be CREATE only? "Unknown Args" #endregion // (!) Shows in API docs for PATCH, but could be CREATE only? "Unknown Args"
[JsonProperty("max_duration")] [JsonProperty("max_duration")]
public int MaxDuration { get; set; } = 30; public int MaxDuration { get; set; } = 60;
[JsonProperty("use_telemetry")] [JsonProperty("use_telemetry")]
public bool UseTelemetry { get; set; } = true; public bool UseTelemetry { get; set; } = true;
@ -70,7 +70,7 @@ public class UpdateAppVersionRequest
public bool InjectContextEnv { get; set; } = true; public bool InjectContextEnv { get; set; } = true;
[JsonProperty("whitelisting_active")] [JsonProperty("whitelisting_active")]
public bool WhitelistingActive { get; set; } = true; public bool WhitelistingActive { get; set; } = false;
[JsonProperty("force_cache")] [JsonProperty("force_cache")]
public bool ForceCache { get; set; } public bool ForceCache { get; set; }
@ -82,7 +82,7 @@ public class UpdateAppVersionRequest
public int CacheMaxHour { get; set; } public int CacheMaxHour { get; set; }
[JsonProperty("time_to_deploy")] [JsonProperty("time_to_deploy")]
public int TimeToDeploy { get; set; } = 15; public int TimeToDeploy { get; set; } = 120;
[JsonProperty("enable_all_locations")] [JsonProperty("enable_all_locations")]
public bool EnableAllLocations { get; set; } public bool EnableAllLocations { get; set; }

View File

@ -237,10 +237,10 @@ private void setVisualElementsToFields()
_containerRegistryFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID); _containerRegistryFoldout = rootVisualElement.Q<Foldout>(EdgegapWindowMetadata.CONTAINER_REGISTRY_FOLDOUT_ID);
_containerNewTagVersionInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_NEW_TAG_VERSION_TXT_ID); _containerNewTagVersionInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_NEW_TAG_VERSION_TXT_ID);
_containerPortNumInput = rootVisualElement.Q<TextField>(EdgegapWindowMetadata.CONTAINER_REGISTRY_PORT_NUM_ID); _containerPortNumInput = rootVisualElement.Q<TextField>(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 // this finds the placeholder and dynamically replaces it with a popup field
VisualElement dropdownPlaceholder = rootVisualElement.Q<VisualElement>("MIRROR_CHANGE_PORT_HARDCODED"); VisualElement dropdownPlaceholder = rootVisualElement.Q<VisualElement>("MIRROR_CHANGE_PORT_HARDCODED");
List<string> options = new List<string> { "UDP", "TCP" }; List<string> options = Enum.GetNames(typeof(ProtocolType)).Cast<string>().ToList();
_containerTransportTypeEnumInput = new PopupField<string>("Protocol Type", options, 0); _containerTransportTypeEnumInput = new PopupField<string>("Protocol Type", options, 0);
dropdownPlaceholder.Add(_containerTransportTypeEnumInput); dropdownPlaceholder.Add(_containerTransportTypeEnumInput);
// END MIRROR CHANGE // END MIRROR CHANGE
@ -1606,6 +1606,7 @@ private async Task buildAndPushServerAsync()
{ {
Port = int.Parse(_containerPortNumInput.value), // OnInputChange clamps + validates, Port = int.Parse(_containerPortNumInput.value), // OnInputChange clamps + validates,
ProtocolStr = _containerTransportTypeEnumInput.value.ToString(), 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.
}, },
}; };

View File

@ -3,10 +3,7 @@ guid: 1c3d4497250ad3e4aa500d4c599b30fe
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2
defaultReferences: defaultReferences: []
- m_ViewDataDictionary: {instanceID: 0}
- LogoImage: {fileID: 2800000, guid: b7012da4ebf9008458abc3ef9a741f3c, type: 3}
- ClipboardImage: {fileID: 2800000, guid: caa516cdb721dd143bbc8000ca78d50a, type: 3}
executionOrder: 0 executionOrder: 0
icon: {instanceID: 0} icon: {instanceID: 0}
userData: userData:

View File

@ -11,7 +11,7 @@ public class InterestManagementTests_Default : InterestManagementTests_Common
public override void ForceHidden_Initial() public override void ForceHidden_Initial()
{ {
// force hide A // force hide A
identityA.visible = Visibility.ForceHidden; identityA.visibility = Visibility.ForceHidden;
// rebuild for both // rebuild for both
// initial rebuild adds all connections if no interest management available // initial rebuild adds all connections if no interest management available
@ -30,7 +30,7 @@ public override void ForceHidden_Initial()
public override void ForceShown_Initial() public override void ForceShown_Initial()
{ {
// force show A // force show A
identityA.visible = Visibility.ForceShown; identityA.visibility = Visibility.ForceShown;
// rebuild for both // rebuild for both
// initial rebuild adds all connections if no interest management available // initial rebuild adds all connections if no interest management available

View File

@ -36,7 +36,7 @@ public override void ForceHidden_Initial()
// A and B are at (0,0,0) so within range! // A and B are at (0,0,0) so within range!
// force hide A // force hide A
identityA.visible = Visibility.ForceHidden; identityA.visibility = Visibility.ForceHidden;
// rebuild for both // rebuild for both
// initial rebuild while both are within range // initial rebuild while both are within range
@ -58,7 +58,7 @@ public override void ForceShown_Initial()
identityB.transform.position = Vector3.right * (aoi.visRange + 1); identityB.transform.position = Vector3.right * (aoi.visRange + 1);
// force show A // force show A
identityA.visible = Visibility.ForceShown; identityA.visibility = Visibility.ForceShown;
// rebuild for both // rebuild for both
// initial rebuild while both are within range // initial rebuild while both are within range

View File

@ -39,7 +39,7 @@ public override void ForceHidden_Initial()
// A and B are at (0,0,0) so within range! // A and B are at (0,0,0) so within range!
// force hide A // force hide A
identityA.visible = Visibility.ForceHidden; identityA.visibility = Visibility.ForceHidden;
// rebuild for both // rebuild for both
// initial rebuild while both are within range // initial rebuild while both are within range
@ -61,7 +61,7 @@ public override void ForceShown_Initial()
identityB.transform.position = Vector3.right * (aoi.visRange + 1); identityB.transform.position = Vector3.right * (aoi.visRange + 1);
// force show A // force show A
identityA.visible = Visibility.ForceShown; identityA.visibility = Visibility.ForceShown;
// update grid now that positions were changed // update grid now that positions were changed
aoi.Update(); aoi.Update();

View File

@ -38,13 +38,6 @@ public void SyncVarsUnityComponent()
"UnityEngine.TextMesh WeaverSyncVarTests.SyncVarsUnityComponent.SyncVarsUnityComponent::invalidVar"); "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] [Test]
public void SyncVarsSyncList() public void SyncVarsSyncList()
{ {

View File

@ -2,9 +2,9 @@
namespace WeaverSyncVarTests.SyncVarsCantBeArray namespace WeaverSyncVarTests.SyncVarsCantBeArray
{ {
class SyncVarsCantBeArray : NetworkBehaviour class SyncVarsCanBeArray : NetworkBehaviour
{ {
[SyncVar] [SyncVar]
int[] thisShouldntWork = new int[100]; int[] thisShouldWork = new int[100];
} }
} }

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d0708030f93e2f498530168e9c644c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -48,8 +48,8 @@ Many of our features quickly became the norm across all Unity netcodes!<br>
| ☁️ **Two Click Hosting** | (Optional) <a href="https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide">Build & Push</a> directly from Unity Editor to the Cloud. | **Preview** | | ☁️ **Two Click Hosting** | (Optional) <a href="https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide">Build & Push</a> directly from Unity Editor to the Cloud. | **Preview** |
| | | | | | | |
| 📏 **Snapshot Interp.** | Perfectly smooth movement for all platforms and all games. | **Stable** | | 📏 **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** | | 🔫 **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! | | | 🧙‍♂️ **General Purpose** | Mirror supports all genres for all your games! | |
| 🧘‍♀️ **Stable API** | Long term (10 years) stability instead of new versions! | | 🧘‍♀️ **Stable API** | Long term (10 years) stability instead of new versions! |