diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs b/Assets/Mirror/Authenticators/BasicAuthenticator.cs index 0fdc7e2..3c077c7 100644 --- a/Assets/Mirror/Authenticators/BasicAuthenticator.cs +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -51,7 +50,7 @@ public override void OnStartServer() /// /// Called on server from StopServer to reset the Authenticator - /// Server message handlers should be registered in this method. + /// Server message handlers should be unregistered in this method. /// public override void OnStopServer() { @@ -60,7 +59,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -153,7 +152,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -163,7 +162,7 @@ public override void OnClientAuthenticate() authPassword = password }; - NetworkClient.connection.Send(authRequestMessage); + NetworkClient.Send(authRequestMessage); } /// diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs index 6723cc9..5e0ea56 100644 --- a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs @@ -4,7 +4,7 @@ namespace Mirror.Authenticators { /// - /// An authenicator that identifies the user by their device. + /// An authenticator that identifies the user by their device. /// A GUID is used as a fallback when the platform doesn't support SystemInfo.deviceUniqueIdentifier. /// Note: deviceUniqueIdentifier can be spoofed, so security is not guaranteed. /// See https://docs.unity3d.com/ScriptReference/SystemInfo-deviceUniqueIdentifier.html for details. @@ -47,7 +47,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -94,7 +94,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -111,7 +111,7 @@ public override void OnClientAuthenticate() } // send the deviceUniqueIdentifier to the server - NetworkClient.connection.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); + NetworkClient.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); } /// diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef index 16cdfbc..70eacf3 100644 --- a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef @@ -1,14 +1,16 @@ { "name": "Mirror.Authenticators", + "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], - "optionalUnityReferences": [], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [] + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs index 89867c1..cb2b01f 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs @@ -15,38 +15,20 @@ public static void AddDefineSymbols() HashSet defines = new HashSet(currentDefines.Split(';')) { "MIRROR", - "MIRROR_17_0_OR_NEWER", - "MIRROR_18_0_OR_NEWER", - "MIRROR_24_0_OR_NEWER", - "MIRROR_26_0_OR_NEWER", - "MIRROR_27_0_OR_NEWER", - "MIRROR_28_0_OR_NEWER", - "MIRROR_29_0_OR_NEWER", - "MIRROR_30_0_OR_NEWER", - "MIRROR_30_5_2_OR_NEWER", - "MIRROR_32_1_2_OR_NEWER", - "MIRROR_32_1_4_OR_NEWER", - "MIRROR_35_0_OR_NEWER", - "MIRROR_35_1_OR_NEWER", - "MIRROR_37_0_OR_NEWER", - "MIRROR_38_0_OR_NEWER", - "MIRROR_39_0_OR_NEWER", - "MIRROR_40_0_OR_NEWER", - "MIRROR_41_0_OR_NEWER", - "MIRROR_42_0_OR_NEWER", - "MIRROR_43_0_OR_NEWER", - "MIRROR_44_0_OR_NEWER", - "MIRROR_46_0_OR_NEWER", - "MIRROR_47_0_OR_NEWER", - "MIRROR_53_0_OR_NEWER", - "MIRROR_55_0_OR_NEWER", "MIRROR_57_0_OR_NEWER", "MIRROR_58_0_OR_NEWER", "MIRROR_65_0_OR_NEWER", - "MIRROR_66_0_OR_NEWER" + "MIRROR_66_0_OR_NEWER", + "MIRROR_2022_9_OR_NEWER", + "MIRROR_2022_10_OR_NEWER", + "MIRROR_70_0_OR_NEWER", + "MIRROR_71_0_OR_NEWER", + "MIRROR_73_OR_NEWER" + // Remove oldest when adding next month's symbol. + // Keep a rolling 12 months of symbols. }; - // only touch PlayerSettings if we actually modified it. + // only touch PlayerSettings if we actually modified it, // otherwise it shows up as changed in git each time. string newDefines = string.Join(";", defines); if (newDefines != currentDefines) diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta index 30806d0..6306c16 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta @@ -5,7 +5,7 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs index ddb38ea..cc9f116 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs @@ -30,7 +30,7 @@ public override void Start() // so make sure we set it here in Start() (after awakes) // Or just let the user assign it in the inspector if (transport == null) - transport = Transport.activeTransport; + transport = Transport.active; base.Start(); } @@ -68,11 +68,9 @@ protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoi throw; } } - #endregion #region Client - /// /// Create a message that will be broadcasted on the network to discover servers /// @@ -108,7 +106,6 @@ protected override void ProcessResponse(ServerResponse response, IPEndPoint endp OnServerFound.Invoke(response); } - #endregion } } diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs index ac57b75..de74fbd 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs @@ -39,6 +39,10 @@ public abstract class NetworkDiscoveryBase : MonoBehaviour [Tooltip("Time in seconds between multi-cast messages")] [Range(1, 60)] float ActiveDiscoveryInterval = 3; + + // broadcast address needs to be configurable on iOS: + // https://github.com/vis2k/Mirror/pull/3255 + public string BroadcastAddress = ""; protected UdpClient serverUdpClient; protected UdpClient clientUdpClient; @@ -368,6 +372,18 @@ public void BroadcastDiscoveryRequest() } IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort); + + if (!string.IsNullOrWhiteSpace(BroadcastAddress)) + { + try + { + endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs index 06a87a9..d9f3042 100644 --- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs +++ b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs @@ -8,16 +8,17 @@ public class NetworkLerpRigidbody : NetworkBehaviour { [Header("Settings")] [SerializeField] internal Rigidbody target = null; + [Tooltip("How quickly current velocity approaches target velocity")] [SerializeField] float lerpVelocityAmount = 0.5f; + [Tooltip("How quickly current position approaches target position")] [SerializeField] float lerpPositionAmount = 0.5f; [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] [SerializeField] bool clientAuthority = false; - float nextSyncTime; - + double nextSyncTime; [SyncVar()] Vector3 targetVelocity; @@ -28,29 +29,22 @@ public class NetworkLerpRigidbody : NetworkBehaviour /// /// Ignore value if is host or client with Authority /// - /// bool IgnoreSync => isServer || ClientWithAuthority; - bool ClientWithAuthority => clientAuthority && hasAuthority; + bool ClientWithAuthority => clientAuthority && isOwned; void OnValidate() { if (target == null) - { target = GetComponent(); - } } void Update() { if (isServer) - { SyncToClients(); - } else if (ClientWithAuthority) - { SendToServer(); - } } void SyncToClients() @@ -61,7 +55,7 @@ void SyncToClients() void SendToServer() { - float now = Time.time; + double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet if (now > nextSyncTime) { nextSyncTime = now + syncInterval; diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs index 4989d29..660adc5 100644 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs @@ -13,7 +13,6 @@ public class NetworkRigidbody : NetworkBehaviour public bool clientAuthority = false; [Header("Velocity")] - [Tooltip("Syncs Velocity every SyncInterval")] [SerializeField] bool syncVelocity = true; @@ -23,9 +22,7 @@ public class NetworkRigidbody : NetworkBehaviour [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] [SerializeField] float velocitySensitivity = 0.1f; - [Header("Angular Velocity")] - [Tooltip("Syncs AngularVelocity every SyncInterval")] [SerializeField] bool syncAngularVelocity = true; @@ -43,13 +40,11 @@ public class NetworkRigidbody : NetworkBehaviour void OnValidate() { if (target == null) - { target = GetComponent(); - } } - #region Sync vars + [SyncVar(hook = nameof(OnVelocityChanged))] Vector3 velocity; @@ -74,7 +69,7 @@ void OnValidate() /// bool IgnoreSync => isServer || ClientWithAuthority; - bool ClientWithAuthority => clientAuthority && hasAuthority; + bool ClientWithAuthority => clientAuthority && isOwned; void OnVelocityChanged(Vector3 _, Vector3 newValue) { @@ -84,7 +79,6 @@ void OnVelocityChanged(Vector3 _, Vector3 newValue) target.velocity = newValue; } - void OnAngularVelocityChanged(Vector3 _, Vector3 newValue) { if (IgnoreSync) @@ -124,32 +118,24 @@ void OnAngularDragChanged(float _, float newValue) target.angularDrag = newValue; } - #endregion + #endregion internal void Update() { if (isServer) - { SyncToClients(); - } else if (ClientWithAuthority) - { SendToServer(); - } } internal void FixedUpdate() { if (clearAngularVelocity && !syncAngularVelocity) - { target.angularVelocity = Vector3.zero; - } if (clearVelocity && !syncVelocity) - { target.velocity = Vector3.zero; - } } /// @@ -191,7 +177,7 @@ void SyncToClients() [Client] void SendToServer() { - if (!hasAuthority) + if (!isOwned) { Debug.LogWarning("SendToServer called without authority"); return; @@ -204,7 +190,7 @@ void SendToServer() [Client] void SendVelocity() { - float now = Time.time; + double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet if (now < previousValue.nextSyncTime) return; @@ -231,9 +217,7 @@ void SendVelocity() // only update syncTime if either has changed if (angularVelocityChanged || velocityChanged) - { previousValue.nextSyncTime = now + syncInterval; - } } [Client] @@ -289,10 +273,9 @@ void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity) if (syncVelocity) { this.velocity = velocity; - target.velocity = velocity; - } + this.angularVelocity = angularVelocity; target.angularVelocity = angularVelocity; } @@ -349,7 +332,7 @@ public class ClientSyncState /// /// Next sync time that velocity will be synced, based on syncInterval. /// - public float nextSyncTime; + public double nextSyncTime; public Vector3 velocity; public Vector3 angularVelocity; public bool isKinematic; diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs index c14b260..5a2c340 100644 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs @@ -3,16 +3,16 @@ namespace Mirror.Experimental { [AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")] public class NetworkRigidbody2D : NetworkBehaviour { [Header("Settings")] [SerializeField] internal Rigidbody2D target = null; [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - public bool clientAuthority = false; + public bool clientAuthority = false; [Header("Velocity")] - [Tooltip("Syncs Velocity every SyncInterval")] [SerializeField] bool syncVelocity = true; @@ -22,9 +22,7 @@ public class NetworkRigidbody2D : NetworkBehaviour [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] [SerializeField] float velocitySensitivity = 0.1f; - [Header("Angular Velocity")] - [Tooltip("Syncs AngularVelocity every SyncInterval")] [SerializeField] bool syncAngularVelocity = true; @@ -42,13 +40,11 @@ public class NetworkRigidbody2D : NetworkBehaviour void OnValidate() { if (target == null) - { target = GetComponent(); - } } - #region Sync vars + [SyncVar(hook = nameof(OnVelocityChanged))] Vector2 velocity; @@ -70,10 +66,9 @@ void OnValidate() /// /// Ignore value if is host or client with Authority /// - /// bool IgnoreSync => isServer || ClientWithAuthority; - bool ClientWithAuthority => clientAuthority && hasAuthority; + bool ClientWithAuthority => clientAuthority && isOwned; void OnVelocityChanged(Vector2 _, Vector2 newValue) { @@ -83,7 +78,6 @@ void OnVelocityChanged(Vector2 _, Vector2 newValue) target.velocity = newValue; } - void OnAngularVelocityChanged(float _, float newValue) { if (IgnoreSync) @@ -123,32 +117,24 @@ void OnAngularDragChanged(float _, float newValue) target.angularDrag = newValue; } - #endregion + #endregion internal void Update() { if (isServer) - { SyncToClients(); - } else if (ClientWithAuthority) - { SendToServer(); - } } internal void FixedUpdate() { if (clearAngularVelocity && !syncAngularVelocity) - { target.angularVelocity = 0f; - } if (clearVelocity && !syncVelocity) - { target.velocity = Vector2.zero; - } } /// @@ -190,7 +176,7 @@ void SyncToClients() [Client] void SendToServer() { - if (!hasAuthority) + if (!isOwned) { Debug.LogWarning("SendToServer called without authority"); return; @@ -227,12 +213,9 @@ void SendVelocity() previousValue.velocity = currentVelocity; } - // only update syncTime if either has changed if (angularVelocityChanged || velocityChanged) - { previousValue.nextSyncTime = now + syncInterval; - } } [Client] @@ -288,9 +271,7 @@ void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity) if (syncVelocity) { this.velocity = velocity; - target.velocity = velocity; - } this.angularVelocity = angularVelocity; target.angularVelocity = angularVelocity; diff --git a/Assets/Mirror/Components/Experimental/NetworkTransform.cs b/Assets/Mirror/Components/Experimental/NetworkTransform.cs deleted file mode 100644 index ca52141..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkTransform.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using UnityEngine; - -namespace Mirror.Experimental -{ - [DisallowMultipleComponent] - // Deprecated 2022-01-18 - [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")] - [AddComponentMenu("")] - public class NetworkTransform : NetworkTransformBase - { - protected override Transform targetTransform => transform; - } -} diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs b/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs deleted file mode 100644 index 8bee5ec..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs +++ /dev/null @@ -1,531 +0,0 @@ -// vis2k: -// base class for NetworkTransform and NetworkTransformChild. -// New method is simple and stupid. No more 1500 lines of code. -// -// Server sends current data. -// Client saves it and interpolates last and latest data points. -// Update handles transform movement / rotation -// FixedUpdate handles rigidbody movement / rotation -// -// Notes: -// * Built-in Teleport detection in case of lags / teleport / obstacles -// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp -// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code. -// * Initial delay might happen if server sends packet immediately after moving -// just 1cm, hence we move 1cm and then wait 100ms for next packet -// * Only way for smooth movement is to use a fixed movement speed during -// interpolation. interpolation over time is never that good. -// -using System; -using UnityEngine; - -namespace Mirror.Experimental -{ - // Deprecated 2022-01-18 - [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")] - public abstract class NetworkTransformBase : NetworkBehaviour - { - // target transform to sync. can be on a child. - protected abstract Transform targetTransform { get; } - - [Header("Authority")] - - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - [SyncVar] - public bool clientAuthority; - - [Tooltip("Set to true if updates from server should be ignored by owner")] - [SyncVar] - public bool excludeOwnerUpdate = true; - - [Header("Synchronization")] - - [Tooltip("Set to true if position should be synchronized")] - [SyncVar] - public bool syncPosition = true; - - [Tooltip("Set to true if rotation should be synchronized")] - [SyncVar] - public bool syncRotation = true; - - [Tooltip("Set to true if scale should be synchronized")] - [SyncVar] - public bool syncScale = true; - - [Header("Interpolation")] - - [Tooltip("Set to true if position should be interpolated")] - [SyncVar] - public bool interpolatePosition = true; - - [Tooltip("Set to true if rotation should be interpolated")] - [SyncVar] - public bool interpolateRotation = true; - - [Tooltip("Set to true if scale should be interpolated")] - [SyncVar] - public bool interpolateScale = true; - - // Sensitivity is added for VR where human players tend to have micro movements so this can quiet down - // the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling. - [Header("Sensitivity")] - - [Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")] - [SyncVar] - public float localPositionSensitivity = .01f; - - [Tooltip("If rotation exceeds this angle, it will be transmitted on the network")] - [SyncVar] - public float localRotationSensitivity = .01f; - - [Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")] - [SyncVar] - public float localScaleSensitivity = .01f; - - [Header("Diagnostics")] - - // server - public Vector3 lastPosition; - public Quaternion lastRotation; - public Vector3 lastScale; - - // client - // use local position/rotation for VR support - [Serializable] - public struct DataPoint - { - public float timeStamp; - public Vector3 localPosition; - public Quaternion localRotation; - public Vector3 localScale; - public float movementSpeed; - - public bool isValid => timeStamp != 0; - } - - // Is this a client with authority over this transform? - // This component could be on the player object or any object that has been assigned authority to this client. - bool IsOwnerWithClientAuthority => hasAuthority && clientAuthority; - - // interpolation start and goal - public DataPoint start = new DataPoint(); - public DataPoint goal = new DataPoint(); - - // We need to store this locally on the server so clients can't request Authority when ever they like - bool clientAuthorityBeforeTeleport; - - void FixedUpdate() - { - // if server then always sync to others. - // let the clients know that this has moved - if (isServer && HasEitherMovedRotatedScaled()) - { - ServerUpdate(); - } - - if (isClient) - { - // send to server if we have local authority (and aren't the server) - // -> only if connectionToServer has been initialized yet too - if (IsOwnerWithClientAuthority) - { - ClientAuthorityUpdate(); - } - else if (goal.isValid) - { - ClientRemoteUpdate(); - } - } - } - - void ServerUpdate() - { - RpcMove(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale); - } - - void ClientAuthorityUpdate() - { - if (!isServer && HasEitherMovedRotatedScaled()) - { - // serialize - // local position/rotation for VR support - // send to server - CmdClientToServerSync(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale); - } - } - - void ClientRemoteUpdate() - { - // teleport or interpolate - if (NeedsTeleport()) - { - // local position/rotation for VR support - ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale); - - // reset data points so we don't keep interpolating - start = new DataPoint(); - goal = new DataPoint(); - } - else - { - // local position/rotation for VR support - ApplyPositionRotationScale(InterpolatePosition(start, goal, targetTransform.localPosition), - InterpolateRotation(start, goal, targetTransform.localRotation), - InterpolateScale(start, goal, targetTransform.localScale)); - } - } - - // moved or rotated or scaled since last time we checked it? - bool HasEitherMovedRotatedScaled() - { - // Save last for next frame to compare only if change was detected, otherwise - // slow moving objects might never sync because of C#'s float comparison tolerance. - // See also: https://github.com/vis2k/Mirror/pull/428) - bool changed = HasMoved || HasRotated || HasScaled; - if (changed) - { - // local position/rotation for VR support - if (syncPosition) lastPosition = targetTransform.localPosition; - if (syncRotation) lastRotation = targetTransform.localRotation; - if (syncScale) lastScale = targetTransform.localScale; - } - return changed; - } - - // local position/rotation for VR support - // SqrMagnitude is faster than Distance per Unity docs - // https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html - - bool HasMoved => syncPosition && Vector3.SqrMagnitude(lastPosition - targetTransform.localPosition) > localPositionSensitivity * localPositionSensitivity; - bool HasRotated => syncRotation && Quaternion.Angle(lastRotation, targetTransform.localRotation) > localRotationSensitivity; - bool HasScaled => syncScale && Vector3.SqrMagnitude(lastScale - targetTransform.localScale) > localScaleSensitivity * localScaleSensitivity; - - // teleport / lag / stuck detection - // - checking distance is not enough since there could be just a tiny fence between us and the goal - // - checking time always works, this way we just teleport if we still didn't reach the goal after too much time has elapsed - bool NeedsTeleport() - { - // calculate time between the two data points - float startTime = start.isValid ? start.timeStamp : Time.time - Time.fixedDeltaTime; - float goalTime = goal.isValid ? goal.timeStamp : Time.time; - float difference = goalTime - startTime; - float timeSinceGoalReceived = Time.time - goalTime; - return timeSinceGoalReceived > difference * 5; - } - - // local authority client sends sync message to server for broadcasting - [Command(channel = Channels.Unreliable)] - void CmdClientToServerSync(Vector3 position, uint packedRotation, Vector3 scale) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - // deserialize payload - SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale); - - // server-only mode does no interpolation to save computations, but let's set the position directly - if (isServer && !isClient) - ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale); - - RpcMove(position, packedRotation, scale); - } - - [ClientRpc(channel = Channels.Unreliable)] - void RpcMove(Vector3 position, uint packedRotation, Vector3 scale) - { - if (hasAuthority && excludeOwnerUpdate) return; - - if (!isServer) - SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale); - } - - // serialization is needed by OnSerialize and by manual sending from authority - void SetGoal(Vector3 position, Quaternion rotation, Vector3 scale) - { - // put it into a data point immediately - DataPoint temp = new DataPoint - { - // deserialize position - localPosition = position, - localRotation = rotation, - localScale = scale, - timeStamp = Time.time - }; - - // movement speed: based on how far it moved since last time has to be calculated before 'start' is overwritten - temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetTransform, Time.fixedDeltaTime); - - // reassign start wisely - // first ever data point? then make something up for previous one so that we can start interpolation without waiting for next. - if (start.timeStamp == 0) - { - start = new DataPoint - { - timeStamp = Time.time - Time.fixedDeltaTime, - // local position/rotation for VR support - localPosition = targetTransform.localPosition, - localRotation = targetTransform.localRotation, - localScale = targetTransform.localScale, - movementSpeed = temp.movementSpeed - }; - } - // second or nth data point? then update previous - // but: we start at where ever we are right now, so that it's perfectly smooth and we don't jump anywhere - // - // example if we are at 'x': - // - // A--x->B - // - // and then receive a new point C: - // - // A--x--B - // | - // | - // C - // - // then we don't want to just jump to B and start interpolation: - // - // x - // | - // | - // C - // - // we stay at 'x' and interpolate from there to C: - // - // x..B - // \ . - // \. - // C - // - else - { - float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition); - float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition); - - start = goal; - - // local position/rotation for VR support - // teleport / lag / obstacle detection: only continue at current position if we aren't too far away - // XC < AB + BC (see comments above) - if (Vector3.Distance(targetTransform.localPosition, start.localPosition) < oldDistance + newDistance) - { - start.localPosition = targetTransform.localPosition; - start.localRotation = targetTransform.localRotation; - start.localScale = targetTransform.localScale; - } - } - - // set new destination in any case. new data is best data. - goal = temp; - } - - // try to estimate movement speed for a data point based on how far it moved since the previous one - // - if this is the first time ever then we use our best guess: - // - delta based on transform.localPosition - // - elapsed based on send interval hoping that it roughly matches - static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval) - { - Vector3 delta = to.localPosition - (from.localPosition != transform.localPosition ? from.localPosition : transform.localPosition); - float elapsed = from.isValid ? to.timeStamp - from.timeStamp : sendInterval; - - // avoid NaN - return elapsed > 0 ? delta.magnitude / elapsed : 0; - } - - // set position carefully depending on the target component - void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale) - { - // local position/rotation for VR support - if (syncPosition) targetTransform.localPosition = position; - if (syncRotation) targetTransform.localRotation = rotation; - if (syncScale) targetTransform.localScale = scale; - } - - // where are we in the timeline between start and goal? [0,1] - Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition) - { - if (!interpolatePosition) - return currentPosition; - - if (start.movementSpeed != 0) - { - // Option 1: simply interpolate based on time, but stutter will happen, it's not that smooth. - // This is especially noticeable if the camera automatically follows the player - // - Tell SonarCloud this isn't really commented code but actual comments and to stfu about it - // - float t = CurrentInterpolationFactor(); - // - return Vector3.Lerp(start.position, goal.position, t); - - // Option 2: always += speed - // speed is 0 if we just started after idle, so always use max for best results - float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed); - return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime); - } - - return currentPosition; - } - - Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation) - { - if (!interpolateRotation) - return defaultRotation; - - if (start.localRotation != goal.localRotation) - { - float t = CurrentInterpolationFactor(start, goal); - return Quaternion.Slerp(start.localRotation, goal.localRotation, t); - } - - return defaultRotation; - } - - Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale) - { - if (!interpolateScale) - return currentScale; - - if (start.localScale != goal.localScale) - { - float t = CurrentInterpolationFactor(start, goal); - return Vector3.Lerp(start.localScale, goal.localScale, t); - } - - return currentScale; - } - - static float CurrentInterpolationFactor(DataPoint start, DataPoint goal) - { - if (start.isValid) - { - float difference = goal.timeStamp - start.timeStamp; - - // the moment we get 'goal', 'start' is supposed to start, so elapsed time is based on: - float elapsed = Time.time - goal.timeStamp; - - // avoid NaN - return difference > 0 ? elapsed / difference : 1; - } - return 1; - } - - #region Server Teleport (force move player) - - /// - /// This method will override this GameObject's current Transform.localPosition to the specified Vector3 and update all clients. - /// NOTE: position must be in LOCAL space if the transform has a parent - /// - /// Where to teleport this GameObject - [Server] - public void ServerTeleport(Vector3 localPosition) - { - Quaternion localRotation = targetTransform.localRotation; - ServerTeleport(localPosition, localRotation); - } - - /// - /// This method will override this GameObject's current Transform.localPosition and Transform.localRotation - /// to the specified Vector3 and Quaternion and update all clients. - /// NOTE: localPosition must be in LOCAL space if the transform has a parent - /// NOTE: localRotation must be in LOCAL space if the transform has a parent - /// - /// Where to teleport this GameObject - /// Which rotation to set this GameObject - [Server] - public void ServerTeleport(Vector3 localPosition, Quaternion localRotation) - { - // To prevent applying the position updates received from client (if they have ClientAuth) while being teleported. - // clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport - // was previously called but not finished therefore we should keep it as true so that 2nd teleport call doesn't clear authority - clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport; - clientAuthority = false; - - DoTeleport(localPosition, localRotation); - - // tell all clients about new values - RpcTeleport(localPosition, Compression.CompressQuaternion(localRotation), clientAuthorityBeforeTeleport); - } - - void DoTeleport(Vector3 newLocalPosition, Quaternion newLocalRotation) - { - targetTransform.localPosition = newLocalPosition; - targetTransform.localRotation = newLocalRotation; - - // Since we are overriding the position we don't need a goal and start. - // Reset them to null for fresh start - goal = new DataPoint(); - start = new DataPoint(); - lastPosition = newLocalPosition; - lastRotation = newLocalRotation; - } - - [ClientRpc(channel = Channels.Unreliable)] - void RpcTeleport(Vector3 newPosition, uint newPackedRotation, bool isClientAuthority) - { - DoTeleport(newPosition, Compression.DecompressQuaternion(newPackedRotation)); - - // only send finished if is owner and is ClientAuthority on server - if (hasAuthority && isClientAuthority) - CmdTeleportFinished(); - } - - /// - /// This RPC will be invoked on server after client finishes overriding the position. - /// - /// - [Command(channel = Channels.Unreliable)] - void CmdTeleportFinished() - { - if (clientAuthorityBeforeTeleport) - { - clientAuthority = true; - - // reset value so doesn't effect future calls, see note in ServerTeleport - clientAuthorityBeforeTeleport = false; - } - else - { - Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this); - } - } - - #endregion - - #region Debug Gizmos - - // draw the data points for easier debugging - void OnDrawGizmos() - { - // draw start and goal points and a line between them - if (start.localPosition != goal.localPosition) - { - DrawDataPointGizmo(start, Color.yellow); - DrawDataPointGizmo(goal, Color.green); - DrawLineBetweenDataPoints(start, goal, Color.cyan); - } - } - - static void DrawDataPointGizmo(DataPoint data, Color color) - { - // use a little offset because transform.localPosition might be in the ground in many cases - Vector3 offset = Vector3.up * 0.01f; - - // draw position - Gizmos.color = color; - Gizmos.DrawSphere(data.localPosition + offset, 0.5f); - - // draw forward and up like unity move tool - Gizmos.color = Color.blue; - Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward); - Gizmos.color = Color.green; - Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up); - } - - static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color) - { - Gizmos.color = color; - Gizmos.DrawLine(data1.localPosition, data2.localPosition); - } - - #endregion - } -} diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs b/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs deleted file mode 100644 index 1ade1de..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using UnityEngine; - -namespace Mirror.Experimental -{ - /// - /// A component to synchronize the position of child transforms of networked objects. - /// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the received values. - /// - // Deprecated 2022-01-18 - [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")] - [AddComponentMenu("")] - public class NetworkTransformChild : NetworkTransformBase - { - [Header("Target")] - public Transform target; - - protected override Transform targetTransform => target; - } -} diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs index 116051b..0441558 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs @@ -8,22 +8,38 @@ namespace Mirror public class DistanceInterestManagement : InterestManagement { [Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")] - public int visRange = 10; + public int visRange = 500; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] public float rebuildInterval = 1; double lastRebuildTime; + // cache custom ranges to avoid runtime TryGetComponent lookups + readonly Dictionary CustomRanges = new Dictionary(); + // helper function to get vis range for a given object, or default. + [ServerCallback] int GetVisRange(NetworkIdentity identity) { - return identity.TryGetComponent(out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; + return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; } [ServerCallback] public override void Reset() { lastRebuildTime = 0D; + CustomRanges.Clear(); + } + + public override void OnSpawned(NetworkIdentity identity) + { + if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom)) + CustomRanges[identity] = custom; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + CustomRanges.Remove(identity); } public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs index 25f5347..12556e5 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs @@ -10,6 +10,6 @@ namespace Mirror public class DistanceInterestManagementCustomRange : NetworkBehaviour { [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 20; + public int visRange = 100; } } diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs index ff2eadc..71238d2 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs @@ -15,34 +15,42 @@ public class MatchInterestManagement : InterestManagement readonly HashSet dirtyMatches = new HashSet(); + [ServerCallback] public override void OnSpawned(NetworkIdentity identity) { - if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) return; - Guid currentMatch = networkMatch.matchId; - lastObjectMatch[identity] = currentMatch; + Guid networkMatchId = networkMatch.matchId; + lastObjectMatch[identity] = networkMatchId; // Guid.Empty is never a valid matchId...do not add to matchObjects collection - if (currentMatch == Guid.Empty) + if (networkMatchId == Guid.Empty) return; // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}"); - if (!matchObjects.TryGetValue(currentMatch, out HashSet objects)) + if (!matchObjects.TryGetValue(networkMatchId, out HashSet objects)) { objects = new HashSet(); - matchObjects.Add(currentMatch, objects); + matchObjects.Add(networkMatchId, objects); } objects.Add(identity); + + // Match 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. + RebuildMatchObservers(networkMatchId); } + [ServerCallback] public override void OnDestroyed(NetworkIdentity identity) { - lastObjectMatch.TryGetValue(identity, out Guid currentMatch); - lastObjectMatch.Remove(identity); - if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity)) - RebuildMatchObservers(currentMatch); + if (lastObjectMatch.TryGetValue(identity, out Guid currentMatch)) + { + lastObjectMatch.Remove(identity); + if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity)) + RebuildMatchObservers(currentMatch); + } } // internal so we can update from tests @@ -53,14 +61,15 @@ internal void Update() // if match changed: // add previous to dirty // add new to dirty - foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) + foreach (NetworkIdentity identity in NetworkServer.spawned.Values) { // Ignore objects that don't have a NetworkMatch component - if (!netIdentity.TryGetComponent(out NetworkMatch networkMatch)) + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) continue; Guid newMatch = networkMatch.matchId; - lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch); + if (!lastObjectMatch.TryGetValue(identity, out Guid currentMatch)) + continue; // Guid.Empty is never a valid matchId // Nothing to do if matchId hasn't changed @@ -72,10 +81,10 @@ internal void Update() // This object is in a new match so observers in the prior match // and the new match need to rebuild their respective observers lists. - UpdateMatchObjects(netIdentity, newMatch, currentMatch); + UpdateMatchObjects(identity, newMatch, currentMatch); } - // rebuild all dirty matchs + // rebuild all dirty matches foreach (Guid dirtyMatch in dirtyMatches) RebuildMatchObservers(dirtyMatch); @@ -119,7 +128,7 @@ void RebuildMatchObservers(Guid matchId) public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Never observed if no NetworkMatch component - if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) + if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) return false; // Guid.Empty is never a valid matchId @@ -127,7 +136,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection return false; // Never observed if no NetworkMatch component - if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) + if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) return false; // Guid.Empty is never a valid matchId @@ -139,7 +148,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) { - if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) return; Guid matchId = networkMatch.matchId; diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs index 8cbfa3b..cc4c1b4 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs @@ -17,6 +17,7 @@ public class SceneInterestManagement : InterestManagement HashSet dirtyScenes = new HashSet(); + [ServerCallback] public override void OnSpawned(NetworkIdentity identity) { Scene currentScene = identity.gameObject.scene; @@ -31,12 +32,15 @@ public override void OnSpawned(NetworkIdentity identity) objects.Add(identity); } + [ServerCallback] public override void OnDestroyed(NetworkIdentity identity) { - Scene currentScene = lastObjectScene[identity]; - lastObjectScene.Remove(identity); - if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) - RebuildSceneObservers(currentScene); + if (lastObjectScene.TryGetValue(identity, out Scene currentScene)) + { + lastObjectScene.Remove(identity); + if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) + RebuildSceneObservers(currentScene); + } } // internal so we can update from tests @@ -49,7 +53,9 @@ internal void Update() // add new to dirty foreach (NetworkIdentity identity in NetworkServer.spawned.Values) { - Scene currentScene = lastObjectScene[identity]; + if (!lastObjectScene.TryGetValue(identity, out Scene currentScene)) + continue; + Scene newScene = identity.gameObject.scene; if (newScene == currentScene) continue; diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs index 88f7197..d557713 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs @@ -1,11 +1,13 @@ // Grid2D from uMMORPG: get/set values of type T at any point // -> not named 'Grid' because Unity already has a Grid type. causes warnings. +// -> struct to avoid memory indirection. it's accessed a lot. using System.Collections.Generic; using UnityEngine; namespace Mirror { - public class Grid2D + // struct to avoid memory indirection. it's accessed a lot. + public struct Grid2D { // the grid // note that we never remove old keys. @@ -16,21 +18,27 @@ public class Grid2D // => makes the code a lot easier too // => this is FINE because in the worst case, every grid position in the // game world is filled with a player anyway! - Dictionary> grid = new Dictionary>(); + readonly Dictionary> grid; // cache a 9 neighbor grid of vector2 offsets so we can use them more easily - Vector2Int[] neighbourOffsets = + readonly Vector2Int[] neighbourOffsets; + + public Grid2D(int initialCapacity) { - Vector2Int.up, - Vector2Int.up + Vector2Int.left, - Vector2Int.up + Vector2Int.right, - Vector2Int.left, - Vector2Int.zero, - Vector2Int.right, - Vector2Int.down, - Vector2Int.down + Vector2Int.left, - Vector2Int.down + Vector2Int.right - }; + grid = new Dictionary>(initialCapacity); + + neighbourOffsets = new[] { + Vector2Int.up, + Vector2Int.up + Vector2Int.left, + Vector2Int.up + Vector2Int.right, + Vector2Int.left, + Vector2Int.zero, + Vector2Int.right, + Vector2Int.down, + Vector2Int.down + Vector2Int.left, + Vector2Int.down + Vector2Int.right + }; + } // helper function so we can add an entry without worrying public void Add(Vector2Int position, T value) @@ -38,7 +46,15 @@ public void Add(Vector2Int position, T value) // initialize set in grid if it's not in there yet if (!grid.TryGetValue(position, out HashSet hashSet)) { + // each grid entry may hold hundreds of entities. + // let's create the HashSet with a large initial capacity + // in order to avoid resizing & allocations. +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have "new HashSet(capacity)" yet hashSet = new HashSet(); +#else + hashSet = new HashSet(128); +#endif grid[position] = hashSet; } diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index eb4c2c5..2986034 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -12,8 +12,17 @@ public class SpatialHashingInterestManagement : InterestManagement [Tooltip("The maximum range that objects will be visible at.")] public int visRange = 30; - // if we see 8 neighbors then 1 entry is visRange/3 - public int resolution => visRange / 3; + // we use a 9 neighbour grid. + // so we always see in a distance of 2 grids. + // for example, our own grid and then one on top / below / left / right. + // + // this means that grid resolution needs to be distance / 2. + // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // + // on first sight, it seems we need distance / 3 (we see left/us/right). + // but that's not the case. + // resolution would be 10, and we only see 1 cell far, so 10+10=20. + public int resolution => visRange / 2; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] public float rebuildInterval = 1; @@ -31,7 +40,8 @@ public enum CheckMethod public bool showSlider; // the grid - Grid2D grid = new Grid2D(); + // begin with a large capacity to avoid resizing & allocations. + Grid2D grid = new Grid2D(1024); // project 3d world position to grid position Vector2Int ProjectToGrid(Vector3 position) => diff --git a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs index e6033ad..ee02a07 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs +++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs @@ -9,9 +9,9 @@ namespace Mirror public class NetworkTeam : NetworkBehaviour { [Tooltip("Set this to the same value on all networked objects that belong to a given team")] - public string teamId = string.Empty; + [SyncVar] public string teamId = string.Empty; [Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")] - public bool forceShown; + [SyncVar] public bool forceShown; } } diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs index 22a8eb0..b543586 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs @@ -6,42 +6,47 @@ 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 Dictionary lastObjectTeam = new Dictionary(); readonly HashSet dirtyTeams = new HashSet(); + [ServerCallback] public override void OnSpawned(NetworkIdentity identity) { - if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) return; - string currentTeam = networkTeam.teamId; - lastObjectTeam[identity] = currentTeam; + string networkTeamId = identityNetworkTeam.teamId; + lastObjectTeam[identity] = networkTeamId; - // string.Empty is never a valid teamId...do not add to teamObjects collection - if (currentTeam == string.Empty) + // Null / Empty string is never a valid teamId...do not add to teamObjects collection + if (string.IsNullOrWhiteSpace(networkTeamId)) return; - // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentTeam}"); - if (!teamObjects.TryGetValue(currentTeam, out HashSet objects)) + //Debug.Log($"TeamInterestManagement.OnSpawned {identity.name} {networkTeamId}"); + + if (!teamObjects.TryGetValue(networkTeamId, out HashSet objects)) { objects = new HashSet(); - teamObjects.Add(currentTeam, objects); + teamObjects.Add(networkTeamId, objects); } objects.Add(identity); + + // 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. + RebuildTeamObservers(networkTeamId); } + [ServerCallback] public override void OnDestroyed(NetworkIdentity identity) { - lastObjectTeam.TryGetValue(identity, out string currentTeam); - lastObjectTeam.Remove(identity); - if (currentTeam != string.Empty && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity)) - RebuildTeamObservers(currentTeam); + if (lastObjectTeam.TryGetValue(identity, out string currentTeam)) + { + lastObjectTeam.Remove(identity); + if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity)) + RebuildTeamObservers(currentTeam); + } } // internal so we can update from tests @@ -55,24 +60,24 @@ internal void Update() foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) { // Ignore objects that don't have a NetworkTeam component - if (!netIdentity.TryGetComponent(out NetworkTeam networkTeam)) + if (!netIdentity.TryGetComponent(out NetworkTeam identityNetworkTeam)) continue; - string newTeam = networkTeam.teamId; + string networkTeamId = identityNetworkTeam.teamId; if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam)) continue; - // string.Empty is never a valid teamId + // Null / Empty string is never a valid teamId // Nothing to do if teamId hasn't changed - if (string.IsNullOrWhiteSpace(newTeam) || newTeam == currentTeam) + if (string.IsNullOrWhiteSpace(networkTeamId) || networkTeamId == currentTeam) continue; // Mark new/old Teams as dirty so they get rebuilt - UpdateDirtyTeams(newTeam, currentTeam); + 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, newTeam, currentTeam); + UpdateTeamObjects(netIdentity, networkTeamId, currentTeam); } // rebuild all dirty teams @@ -84,8 +89,8 @@ internal void Update() void UpdateDirtyTeams(string newTeam, string currentTeam) { - // string.Empty is never a valid teamId - if (currentTeam != string.Empty) + // Null / Empty string is never a valid teamId + if (!string.IsNullOrWhiteSpace(currentTeam)) dirtyTeams.Add(currentTeam); dirtyTeams.Add(newTeam); @@ -119,7 +124,7 @@ void RebuildTeamObservers(string teamId) public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Always observed if no NetworkTeam component - if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) + if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) return true; if (identityNetworkTeam.forceShown) @@ -130,16 +135,15 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection return false; // Always observed if no NetworkTeam component - if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam)) - return true; - - if (newObserverNetworkTeam.forceShown) + if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam)) return true; - // string.Empty is never a valid teamId + // Null / Empty string is never a valid teamId if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId)) return false; + //Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}"); + // Observed only if teamId's match return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId; } @@ -147,7 +151,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) { // If this object doesn't have a NetworkTeam then it's visible to all clients - if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) { AddAllConnections(newObservers); return; @@ -160,8 +164,8 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet - /// Custom Serialization - /// - /// - /// - /// - public override bool OnSerialize(NetworkWriter writer, bool initialState) + public override void OnSerialize(NetworkWriter writer, bool initialState) { - bool changed = base.OnSerialize(writer, initialState); + base.OnSerialize(writer, initialState); if (initialState) { for (int i = 0; i < animator.layerCount; i++) @@ -390,16 +384,9 @@ public override bool OnSerialize(NetworkWriter writer, bool initialState) writer.WriteFloat(animator.GetLayerWeight(i)); } WriteParameters(writer, initialState); - return true; } - return changed; } - /// - /// Custom Deserialization - /// - /// - /// public override void OnDeserialize(NetworkReader reader, bool initialState) { base.OnDeserialize(reader, initialState); @@ -441,7 +428,7 @@ public void SetTrigger(int hash) return; } - if (!hasAuthority) + if (!isOwned) { Debug.LogWarning("Only the client with authority can set animations"); return; @@ -476,9 +463,7 @@ public void ResetTrigger(string triggerName) ResetTrigger(Animator.StringToHash(triggerName)); } - /// - /// Causes an animation trigger to be reset for a networked object. - /// + /// Causes an animation trigger to be reset for a networked object. /// Hash id of trigger (from the Animator). public void ResetTrigger(int hash) { @@ -490,7 +475,7 @@ public void ResetTrigger(int hash) return; } - if (!hasAuthority) + if (!isOwned) { Debug.LogWarning("Only the client with authority can reset animations"); return; @@ -558,7 +543,7 @@ void CmdOnAnimationTriggerServerMessage(int hash) // handle and broadcast // host should have already the trigger - bool isHostOwner = isClient && hasAuthority; + bool isHostOwner = isClient && isOwned; if (!isHostOwner) { HandleAnimTriggerMsg(hash); @@ -576,7 +561,7 @@ void CmdOnAnimationResetTriggerServerMessage(int hash) // handle and broadcast // host should have already the trigger - bool isHostOwner = isClient && hasAuthority; + bool isHostOwner = isClient && isOwned; if (!isHostOwner) { HandleAnimResetTriggerMsg(hash); @@ -615,7 +600,7 @@ void RpcOnAnimationParametersClientMessage(byte[] parameters) void RpcOnAnimationTriggerClientMessage(int hash) { // host/owner handles this before it is sent - if (isServer || (clientAuthority && hasAuthority)) return; + if (isServer || (clientAuthority && isOwned)) return; HandleAnimTriggerMsg(hash); } @@ -624,7 +609,7 @@ void RpcOnAnimationTriggerClientMessage(int hash) void RpcOnAnimationResetTriggerClientMessage(int hash) { // host/owner handles this before it is sent - if (isServer || (clientAuthority && hasAuthority)) return; + if (isServer || (clientAuthority && isOwned)) return; HandleAnimResetTriggerMsg(hash); } diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs b/Assets/Mirror/Components/NetworkPingDisplay.cs index 61e9241..156a48c 100644 --- a/Assets/Mirror/Components/NetworkPingDisplay.cs +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs @@ -13,8 +13,8 @@ public class NetworkPingDisplay : MonoBehaviour { public Color color = Color.white; public int padding = 2; - int width = 150; - int height = 25; + public int width = 150; + public int height = 25; void OnGUI() { diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs b/Assets/Mirror/Components/NetworkRoomManager.cs index d432fbb..bde8180 100644 --- a/Assets/Mirror/Components/NetworkRoomManager.cs +++ b/Assets/Mirror/Components/NetworkRoomManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -22,11 +21,10 @@ public class NetworkRoomManager : NetworkManager public struct PendingPlayer { public NetworkConnectionToClient conn; - public GameObject roomPlayer; + public GameObject roomPlayer; } [Header("Room Settings")] - [FormerlySerializedAs("m_ShowRoomGUI")] [SerializeField] [Tooltip("This flag controls whether the default UI is shown for the room")] @@ -61,7 +59,6 @@ public struct PendingPlayer public List pendingPlayers = new List(); [Header("Diagnostics")] - /// /// True when all players have submitted a Ready message /// @@ -102,8 +99,7 @@ public bool allPlayersReady public override void OnValidate() { - // always >= 0 - maxConnections = Mathf.Max(maxConnections, 0); + base.OnValidate(); // always <= maxConnections minPlayers = Mathf.Min(minPlayers, maxConnections); @@ -120,8 +116,6 @@ public override void OnValidate() Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component."); } } - - base.OnValidate(); } public void ReadyStatusChanged() @@ -169,7 +163,7 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer) { Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}"); - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { // cant be ready in room, add to ready list PendingPlayer pending; @@ -202,7 +196,7 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer) /// public void CheckReadyToBegin() { - if (!IsSceneActive(RoomScene)) + if (!Utils.IsSceneActive(RoomScene)) return; int numberOfReadyPlayers = NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.identity.gameObject.GetComponent().readyToBegin); @@ -247,15 +241,10 @@ internal void CallOnClientExitRoom() /// Connection from client. public override void OnServerConnect(NetworkConnectionToClient conn) { - if (numPlayers >= maxConnections) - { - conn.Disconnect(); - return; - } - // cannot join game in progress - if (!IsSceneActive(RoomScene)) + if (!Utils.IsSceneActive(RoomScene)) { + Debug.Log($"Not in Room scene...disconnecting {conn}"); conn.Disconnect(); return; } @@ -278,7 +267,7 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) if (roomPlayer != null) roomSlots.Remove(roomPlayer); - foreach (NetworkIdentity clientOwnedObject in conn.clientOwnedObjects) + foreach (NetworkIdentity clientOwnedObject in conn.owned) { roomPlayer = clientOwnedObject.GetComponent(); if (roomPlayer != null) @@ -294,7 +283,7 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) player.GetComponent().readyToBegin = false; } - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) RecalculateRoomPlayerIndices(); OnRoomServerDisconnect(conn); @@ -319,11 +308,8 @@ public override void OnServerAddPlayer(NetworkConnectionToClient conn) // increment the index before adding the player, so first player starts at 1 clientIndex++; - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { - if (roomSlots.Count == maxConnections) - return; - allPlayersReady = false; //Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}"); @@ -335,7 +321,11 @@ public override void OnServerAddPlayer(NetworkConnectionToClient conn) NetworkServer.AddPlayerForConnection(conn, newRoomGameObject); } else - OnRoomServerAddPlayer(conn); + { + // Late joiners not supported...should've been kicked by OnServerDisconnect + Debug.Log($"Not in Room scene...disconnecting {conn}"); + conn.Disconnect(); + } } [Server] @@ -472,10 +462,7 @@ public override void OnStartClient() /// public override void OnClientConnect() { -#pragma warning disable 618 - // obsolete method calls new method - OnRoomClientConnect(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientConnect(); base.OnClientConnect(); } @@ -485,9 +472,7 @@ public override void OnClientConnect() /// public override void OnClientDisconnect() { -#pragma warning disable 618 - OnRoomClientDisconnect(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientDisconnect(); base.OnClientDisconnect(); } @@ -507,7 +492,7 @@ public override void OnStopClient() /// public override void OnClientSceneChanged() { - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { if (NetworkClient.isConnected) CallOnClientEnterRoom(); @@ -516,10 +501,7 @@ public override void OnClientSceneChanged() CallOnClientExitRoom(); base.OnClientSceneChanged(); -#pragma warning disable 618 - // obsolete method calls new method - OnRoomClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientSceneChanged(); } #endregion @@ -647,19 +629,11 @@ public virtual void OnRoomClientExit() {} /// public virtual void OnRoomClientConnect() {} - // Deprecated 2021-10-30 - [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] - public virtual void OnRoomClientConnect(NetworkConnection conn) => OnRoomClientConnect(); - /// /// This is called on the client when disconnected from a server. /// public virtual void OnRoomClientDisconnect() {} - // Deprecated 2021-10-30 - [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] - public virtual void OnRoomClientDisconnect(NetworkConnection conn) => OnRoomClientDisconnect(); - /// /// This is called on the client when a client is started. /// @@ -675,16 +649,6 @@ public virtual void OnRoomStopClient() {} /// public virtual void OnRoomClientSceneChanged() {} - // Deprecated 2021-10-30 - [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] - public virtual void OnRoomClientSceneChanged(NetworkConnection conn) => OnRoomClientSceneChanged(); - - /// - /// Called on the client when adding a player to the room fails. - /// This could be because the room is full, or the connection is not allowed to have more players. - /// - public virtual void OnRoomClientAddPlayerFailed() {} - #endregion #region optional UI @@ -697,7 +661,7 @@ public virtual void OnGUI() if (!showRoomGUI) return; - if (NetworkServer.active && IsSceneActive(GameplayScene)) + if (NetworkServer.active && Utils.IsSceneActive(GameplayScene)) { GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f)); if (GUILayout.Button("Return to Room")) @@ -705,7 +669,7 @@ public virtual void OnGUI() GUILayout.EndArea(); } - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS"); } diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs b/Assets/Mirror/Components/NetworkRoomPlayer.cs index d2763d5..9f5e158 100644 --- a/Assets/Mirror/Components/NetworkRoomPlayer.cs +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs @@ -139,7 +139,7 @@ public virtual void OnGUI() if (!room.showRoomGUI) return; - if (!NetworkManager.IsSceneActive(room.RoomScene)) + if (!Utils.IsSceneActive(room.RoomScene)) return; DrawPlayerReadyState(); diff --git a/Assets/Mirror/Components/NetworkStatistics.cs b/Assets/Mirror/Components/NetworkStatistics.cs index a95d4a9..5d09fd0 100644 --- a/Assets/Mirror/Components/NetworkStatistics.cs +++ b/Assets/Mirror/Components/NetworkStatistics.cs @@ -4,9 +4,11 @@ namespace Mirror { /// - /// Shows Network messages and bytes sent & received per second. - /// Add this component to the same object as Network Manager. + /// Shows Network messages and bytes sent and received per second. /// + /// + /// Add this component to the same object as Network Manager. + /// [AddComponentMenu("Network/Network Statistics")] [DisallowMultipleComponent] [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")] @@ -17,43 +19,43 @@ public class NetworkStatistics : MonoBehaviour // --------------------------------------------------------------------- - // CLIENT + // CLIENT (public fields for other components to grab statistics) // long bytes to support >2GB - int clientIntervalReceivedPackets; - long clientIntervalReceivedBytes; - int clientIntervalSentPackets; - long clientIntervalSentBytes; + [HideInInspector] public int clientIntervalReceivedPackets; + [HideInInspector] public long clientIntervalReceivedBytes; + [HideInInspector] public int clientIntervalSentPackets; + [HideInInspector] public long clientIntervalSentBytes; // results from last interval // long bytes to support >2GB - int clientReceivedPacketsPerSecond; - long clientReceivedBytesPerSecond; - int clientSentPacketsPerSecond; - long clientSentBytesPerSecond; + [HideInInspector] public int clientReceivedPacketsPerSecond; + [HideInInspector] public long clientReceivedBytesPerSecond; + [HideInInspector] public int clientSentPacketsPerSecond; + [HideInInspector] public long clientSentBytesPerSecond; // --------------------------------------------------------------------- - // SERVER + // SERVER (public fields for other components to grab statistics) // capture interval // long bytes to support >2GB - int serverIntervalReceivedPackets; - long serverIntervalReceivedBytes; - int serverIntervalSentPackets; - long serverIntervalSentBytes; + [HideInInspector] public int serverIntervalReceivedPackets; + [HideInInspector] public long serverIntervalReceivedBytes; + [HideInInspector] public int serverIntervalSentPackets; + [HideInInspector] public long serverIntervalSentBytes; // results from last interval // long bytes to support >2GB - int serverReceivedPacketsPerSecond; - long serverReceivedBytesPerSecond; - int serverSentPacketsPerSecond; - long serverSentBytesPerSecond; + [HideInInspector] public int serverReceivedPacketsPerSecond; + [HideInInspector] public long serverReceivedBytesPerSecond; + [HideInInspector] public int serverSentPacketsPerSecond; + [HideInInspector] public long serverSentBytesPerSecond; - // NetworkManager sets Transport.activeTransport in Awake(). + // NetworkManager sets Transport.active in Awake(). // so let's hook into it in Start(). void Start() { // find available transport - Transport transport = Transport.activeTransport; + Transport transport = Transport.active; if (transport != null) { transport.OnClientDataReceived += OnClientReceive; @@ -67,7 +69,7 @@ void Start() void OnDestroy() { // remove transport hooks - Transport transport = Transport.activeTransport; + Transport transport = Transport.active; if (transport != null) { transport.OnClientDataReceived -= OnClientReceive; diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs deleted file mode 100644 index b7b8e81..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ʻOumuamua's light curve, assuming little systematic error, presents its -// motion as tumbling, rather than smoothly rotating, and moving sufficiently -// fast relative to the Sun. -// -// A small number of astronomers suggested that ʻOumuamua could be a product of -// alien technology, but evidence in support of this hypothesis is weak. -using UnityEngine; - -namespace Mirror -{ - [DisallowMultipleComponent] - [AddComponentMenu("Network/Network Transform")] - public class NetworkTransform : NetworkTransformBase - { - protected override Transform targetComponent => transform; - } -} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs deleted file mode 100644 index 54e77a7..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs +++ /dev/null @@ -1,776 +0,0 @@ -// NetworkTransform V2 aka project Oumuamua by vis2k (2021-07) -// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ -// -// Base class for NetworkTransform and NetworkTransformChild. -// => simple unreliable sync without any interpolation for now. -// => which means we don't need teleport detection either -// -// NOTE: several functions are virtual in case someone needs to modify a part. -// -// Channel: uses UNRELIABLE at all times. -// -> out of order packets are dropped automatically -// -> it's better than RELIABLE for several reasons: -// * head of line blocking would add delay -// * resending is mostly pointless -// * bigger data race: -// -> if we use a Cmd() at position X over reliable -// -> client gets Cmd() and X at the same time, but buffers X for bufferTime -// -> for unreliable, it would get X before the reliable Cmd(), still -// buffer for bufferTime but end up closer to the original time -// comment out the below line to quickly revert the onlySyncOnChange feature -#define onlySyncOnChange_BANDWIDTH_SAVING -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace Mirror -{ - public abstract class NetworkTransformBase : NetworkBehaviour - { - // TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier? - [Header("Authority")] - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - public bool clientAuthority; - - // Is this a client with authority over this transform? - // This component could be on the player object or any object that has been assigned authority to this client. - protected bool IsClientWithAuthority => hasAuthority && clientAuthority; - - // target transform to sync. can be on a child. - protected abstract Transform targetComponent { get; } - - [Header("Synchronization")] - [Range(0, 1)] public float sendInterval = 0.050f; - public bool syncPosition = true; - public bool syncRotation = true; - // scale sync is rare. off by default. - public bool syncScale = false; - - double lastClientSendTime; - double lastServerSendTime; - - // not all games need to interpolate. a board game might jump to the - // final position immediately. - [Header("Interpolation")] - public bool interpolatePosition = true; - public bool interpolateRotation = true; - public bool interpolateScale = false; - - // "Experimentally I’ve found that the amount of delay that works best - // at 2-5% packet loss is 3X the packet send rate" - // NOTE: we do NOT use a dyanmically changing buffer size. - // it would come with a lot of complications, e.g. buffer time - // advantages/disadvantages for different connections. - // Glenn Fiedler's recommendation seems solid, and should cover - // the vast majority of connections. - // (a player with 2000ms latency will have issues no matter what) - [Header("Buffering")] - [Tooltip("Snapshots are buffered for sendInterval * multiplier seconds. If your expected client base is to run at non-ideal connection quality (2-5% packet loss), 3x supposedly works best.")] - public int bufferTimeMultiplier = 1; - public float bufferTime => sendInterval * bufferTimeMultiplier; - [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")] - public int bufferSizeLimit = 64; - - [Tooltip("Start to accelerate interpolation if buffer size is >= threshold. Needs to be larger than bufferTimeMultiplier.")] - public int catchupThreshold = 4; - - [Tooltip("Once buffer is larger catchupThreshold, accelerate by multiplier % per excess entry.")] - [Range(0, 1)] public float catchupMultiplier = 0.10f; - -#if onlySyncOnChange_BANDWIDTH_SAVING - [Header("Sync Only If Changed")] - [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] - public bool onlySyncOnChange = true; - - // 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching. - [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")] - public float bufferResetMultiplier = 5; - - [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] - public float positionSensitivity = 0.01f; - public float rotationSensitivity = 0.01f; - public float scaleSensitivity = 0.01f; - - protected bool positionChanged; - protected bool rotationChanged; - protected bool scaleChanged; - - // Used to store last sent snapshots - protected NTSnapshot lastSnapshot; - protected bool cachedSnapshotComparison; - protected bool hasSentUnchangedPosition; -#endif - - // snapshots sorted by timestamp - // in the original article, glenn fiedler drops any snapshots older than - // the last received snapshot. - // -> instead, we insert into a sorted buffer - // -> the higher the buffer information density, the better - // -> we still drop anything older than the first element in the buffer - // => internal for testing - // - // IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot' - // interface because List allocates through boxing - internal SortedList serverBuffer = new SortedList(); - internal SortedList clientBuffer = new SortedList(); - - // absolute interpolation time, moved along with deltaTime - // (roughly between [0, delta] where delta is snapshot B - A timestamp) - // (can be bigger than delta when overshooting) - double serverInterpolationTime; - double clientInterpolationTime; - - // only convert the static Interpolation function to Func once to - // avoid allocations - Func Interpolate = NTSnapshot.Interpolate; - - [Header("Debug")] - public bool showGizmos; - public bool showOverlay; - public Color overlayColor = new Color(0, 0, 0, 0.5f); - - // snapshot functions ////////////////////////////////////////////////// - // construct a snapshot of the current state - // => internal for testing - protected virtual NTSnapshot ConstructSnapshot() - { - // NetworkTime.localTime for double precision until Unity has it too - return new NTSnapshot( - // our local time is what the other end uses as remote time - NetworkTime.localTime, - // the other end fills out local time itself - 0, - targetComponent.localPosition, - targetComponent.localRotation, - targetComponent.localScale - ); - } - - // apply a snapshot to the Transform. - // -> start, end, interpolated are all passed in caes they are needed - // -> a regular game would apply the 'interpolated' snapshot - // -> a board game might want to jump to 'goal' directly - // (it's easier to always interpolate and then apply selectively, - // instead of manually interpolating x, y, z, ... depending on flags) - // => internal for testing - // - // NOTE: stuck detection is unnecessary here. - // we always set transform.position anyway, we can't get stuck. - protected virtual void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated) - { - // local position/rotation for VR support - // - // if syncPosition/Rotation/Scale is disabled then we received nulls - // -> current position/rotation/scale would've been added as snapshot - // -> we still interpolated - // -> but simply don't apply it. if the user doesn't want to sync - // scale, then we should not touch scale etc. - if (syncPosition) - targetComponent.localPosition = interpolatePosition ? interpolated.position : goal.position; - - if (syncRotation) - targetComponent.localRotation = interpolateRotation ? interpolated.rotation : goal.rotation; - - if (syncScale) - targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale; - } -#if onlySyncOnChange_BANDWIDTH_SAVING - // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. - protected virtual bool CompareSnapshots(NTSnapshot currentSnapshot) - { - positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; - rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; - scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity; - - return (!positionChanged && !rotationChanged && !scaleChanged); - } -#endif - // cmd ///////////////////////////////////////////////////////////////// - // only unreliable. see comment above of this file. - [Command(channel = Channels.Unreliable)] - void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) - { - OnClientToServerSync(position, rotation, scale); - //For client authority, immediately pass on the client snapshot to all other - //clients instead of waiting for server to send its snapshots. - if (clientAuthority) - { - RpcServerToClientSync(position, rotation, scale); - } - } - - // local authority client sends sync message to server for broadcasting - protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) - { - // only apply if in client authority mode - if (!clientAuthority) return; - - // protect against ever growing buffer size attacks - if (serverBuffer.Count >= bufferSizeLimit) return; - - // only player owned objects (with a connection) can send to - // server. we can get the timestamp from the connection. - double timestamp = connectionToClient.remoteTimeStamp; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (onlySyncOnChange) - { - double timeIntervalCheck = bufferResetMultiplier * sendInterval; - - if (serverBuffer.Count > 0 && serverBuffer.Values[serverBuffer.Count - 1].remoteTimestamp + timeIntervalCheck < timestamp) - { - Reset(); - } - } -#endif - // position, rotation, scale can have no value if same as last time. - // saves bandwidth. - // but we still need to feed it to snapshot interpolation. we can't - // just have gaps in there if nothing has changed. for example, if - // client sends snapshot at t=0 - // client sends nothing for 10s because not moved - // client sends snapshot at t=10 - // then the server would assume that it's one super slow move and - // replay it for 10 seconds. - if (!position.HasValue) position = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].position : targetComponent.localPosition; - if (!rotation.HasValue) rotation = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].rotation : targetComponent.localRotation; - if (!scale.HasValue) scale = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].scale : targetComponent.localScale; - - // construct snapshot with batch timestamp to save bandwidth - NTSnapshot snapshot = new NTSnapshot( - timestamp, - NetworkTime.localTime, - position.Value, rotation.Value, scale.Value - ); - - // add to buffer (or drop if older than first element) - SnapshotInterpolation.InsertIfNewEnough(snapshot, serverBuffer); - } - - // rpc ///////////////////////////////////////////////////////////////// - // only unreliable. see comment above of this file. - [ClientRpc(channel = Channels.Unreliable)] - void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) => - OnServerToClientSync(position, rotation, scale); - - // server broadcasts sync message to all clients - protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) - { - // in host mode, the server sends rpcs to all clients. - // the host client itself will receive them too. - // -> host server is always the source of truth - // -> we can ignore any rpc on the host client - // => otherwise host objects would have ever growing clientBuffers - // (rpc goes to clients. if isServer is true too then we are host) - if (isServer) return; - - // don't apply for local player with authority - if (IsClientWithAuthority) return; - - // protect against ever growing buffer size attacks - if (clientBuffer.Count >= bufferSizeLimit) return; - - // on the client, we receive rpcs for all entities. - // not all of them have a connectionToServer. - // but all of them go through NetworkClient.connection. - // we can get the timestamp from there. - double timestamp = NetworkClient.connection.remoteTimeStamp; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (onlySyncOnChange) - { - double timeIntervalCheck = bufferResetMultiplier * sendInterval; - - if (clientBuffer.Count > 0 && clientBuffer.Values[clientBuffer.Count - 1].remoteTimestamp + timeIntervalCheck < timestamp) - { - Reset(); - } - } -#endif - // position, rotation, scale can have no value if same as last time. - // saves bandwidth. - // but we still need to feed it to snapshot interpolation. we can't - // just have gaps in there if nothing has changed. for example, if - // client sends snapshot at t=0 - // client sends nothing for 10s because not moved - // client sends snapshot at t=10 - // then the server would assume that it's one super slow move and - // replay it for 10 seconds. - if (!position.HasValue) position = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].position : targetComponent.localPosition; - if (!rotation.HasValue) rotation = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].rotation : targetComponent.localRotation; - if (!scale.HasValue) scale = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].scale : targetComponent.localScale; - - // construct snapshot with batch timestamp to save bandwidth - NTSnapshot snapshot = new NTSnapshot( - timestamp, - NetworkTime.localTime, - position.Value, rotation.Value, scale.Value - ); - - // add to buffer (or drop if older than first element) - SnapshotInterpolation.InsertIfNewEnough(snapshot, clientBuffer); - } - - // update ////////////////////////////////////////////////////////////// - void UpdateServer() - { - // broadcast to all clients each 'sendInterval' - // (client with authority will drop the rpc) - // NetworkTime.localTime for double precision until Unity has it too - // - // IMPORTANT: - // snapshot interpolation requires constant sending. - // DO NOT only send if position changed. for example: - // --- - // * client sends first position at t=0 - // * ... 10s later ... - // * client moves again, sends second position at t=10 - // --- - // * server gets first position at t=0 - // * server gets second position at t=10 - // * server moves from first to second within a time of 10s - // => would be a super slow move, instead of a wait & move. - // - // IMPORTANT: - // DO NOT send nulls if not changed 'since last send' either. we - // send unreliable and don't know which 'last send' the other end - // received successfully. - // - // Checks to ensure server only sends snapshots if object is - // on server authority(!clientAuthority) mode because on client - // authority mode snapshots are broadcasted right after the authoritative - // client updates server in the command function(see above), OR, - // since host does not send anything to update the server, any client - // authoritative movement done by the host will have to be broadcasted - // here by checking IsClientWithAuthority. - if (NetworkTime.localTime >= lastServerSendTime + sendInterval && - (!clientAuthority || IsClientWithAuthority)) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - NTSnapshot snapshot = ConstructSnapshot(); -#if onlySyncOnChange_BANDWIDTH_SAVING - cachedSnapshotComparison = CompareSnapshots(snapshot); - if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } -#endif - -#if onlySyncOnChange_BANDWIDTH_SAVING - RpcServerToClientSync( - // only sync what the user wants to sync - syncPosition && positionChanged ? snapshot.position : default(Vector3?), - syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), - syncScale && scaleChanged ? snapshot.scale : default(Vector3?) - ); -#else - RpcServerToClientSync( - // only sync what the user wants to sync - syncPosition ? snapshot.position : default(Vector3?), - syncRotation ? snapshot.rotation : default(Quaternion?), - syncScale ? snapshot.scale : default(Vector3?) - ); -#endif - - lastServerSendTime = NetworkTime.localTime; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (cachedSnapshotComparison) - { - hasSentUnchangedPosition = true; - } - else - { - hasSentUnchangedPosition = false; - lastSnapshot = snapshot; - } -#endif - } - - // apply buffered snapshots IF client authority - // -> in server authority, server moves the object - // so no need to apply any snapshots there. - // -> don't apply for host mode player objects either, even if in - // client authority mode. if it doesn't go over the network, - // then we don't need to do anything. - if (clientAuthority && !hasAuthority) - { - // compute snapshot interpolation & apply if any was spit out - // TODO we don't have Time.deltaTime double yet. float is fine. - if (SnapshotInterpolation.Compute( - NetworkTime.localTime, Time.deltaTime, - ref serverInterpolationTime, - bufferTime, serverBuffer, - catchupThreshold, catchupMultiplier, - Interpolate, - out NTSnapshot computed)) - { - NTSnapshot start = serverBuffer.Values[0]; - NTSnapshot goal = serverBuffer.Values[1]; - ApplySnapshot(start, goal, computed); - } - } - } - - void UpdateClient() - { - // client authority, and local player (= allowed to move myself)? - if (IsClientWithAuthority) - { - // https://github.com/vis2k/Mirror/pull/2992/ - if (!NetworkClient.ready) return; - - // send to server each 'sendInterval' - // NetworkTime.localTime for double precision until Unity has it too - // - // IMPORTANT: - // snapshot interpolation requires constant sending. - // DO NOT only send if position changed. for example: - // --- - // * client sends first position at t=0 - // * ... 10s later ... - // * client moves again, sends second position at t=10 - // --- - // * server gets first position at t=0 - // * server gets second position at t=10 - // * server moves from first to second within a time of 10s - // => would be a super slow move, instead of a wait & move. - // - // IMPORTANT: - // DO NOT send nulls if not changed 'since last send' either. we - // send unreliable and don't know which 'last send' the other end - // received successfully. - if (NetworkTime.localTime >= lastClientSendTime + sendInterval) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - NTSnapshot snapshot = ConstructSnapshot(); -#if onlySyncOnChange_BANDWIDTH_SAVING - cachedSnapshotComparison = CompareSnapshots(snapshot); - if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } -#endif - -#if onlySyncOnChange_BANDWIDTH_SAVING - CmdClientToServerSync( - // only sync what the user wants to sync - syncPosition && positionChanged ? snapshot.position : default(Vector3?), - syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), - syncScale && scaleChanged ? snapshot.scale : default(Vector3?) - ); -#else - CmdClientToServerSync( - // only sync what the user wants to sync - syncPosition ? snapshot.position : default(Vector3?), - syncRotation ? snapshot.rotation : default(Quaternion?), - syncScale ? snapshot.scale : default(Vector3?) - ); -#endif - - lastClientSendTime = NetworkTime.localTime; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (cachedSnapshotComparison) - { - hasSentUnchangedPosition = true; - } - else - { - hasSentUnchangedPosition = false; - lastSnapshot = snapshot; - } -#endif - } - } - // for all other clients (and for local player if !authority), - // we need to apply snapshots from the buffer - else - { - // compute snapshot interpolation & apply if any was spit out - // TODO we don't have Time.deltaTime double yet. float is fine. - if (SnapshotInterpolation.Compute( - NetworkTime.localTime, Time.deltaTime, - ref clientInterpolationTime, - bufferTime, clientBuffer, - catchupThreshold, catchupMultiplier, - Interpolate, - out NTSnapshot computed)) - { - NTSnapshot start = clientBuffer.Values[0]; - NTSnapshot goal = clientBuffer.Values[1]; - ApplySnapshot(start, goal, computed); - } - } - } - - void Update() - { - // if server then always sync to others. - if (isServer) UpdateServer(); - // 'else if' because host mode shouldn't send anything to server. - // it is the server. don't overwrite anything there. - else if (isClient) UpdateClient(); - } - - // common Teleport code for client->server and server->client - protected virtual void OnTeleport(Vector3 destination) - { - // reset any in-progress interpolation & buffers - Reset(); - - // set the new position. - // interpolation will automatically continue. - targetComponent.position = destination; - - // TODO - // what if we still receive a snapshot from before the interpolation? - // it could easily happen over unreliable. - // -> maybe add destionation as first entry? - } - - // common Teleport code for client->server and server->client - protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) - { - // reset any in-progress interpolation & buffers - Reset(); - - // set the new position. - // interpolation will automatically continue. - targetComponent.position = destination; - targetComponent.rotation = rotation; - - // TODO - // what if we still receive a snapshot from before the interpolation? - // it could easily happen over unreliable. - // -> maybe add destionation as first entry? - } - - // server->client teleport to force position without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [ClientRpc] - public void RpcTeleport(Vector3 destination) - { - // NOTE: even in client authority mode, the server is always allowed - // to teleport the player. for example: - // * CmdEnterPortal() might teleport the player - // * Some people use client authority with server sided checks - // so the server should be able to reset position if needed. - - // TODO what about host mode? - OnTeleport(destination); - } - - // server->client teleport to force position and rotation without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [ClientRpc] - public void RpcTeleport(Vector3 destination, Quaternion rotation) - { - // NOTE: even in client authority mode, the server is always allowed - // to teleport the player. for example: - // * CmdEnterPortal() might teleport the player - // * Some people use client authority with server sided checks - // so the server should be able to reset position if needed. - - // TODO what about host mode? - OnTeleport(destination, rotation); - } - - // Deprecated 2022-01-19 - [Obsolete("Use RpcTeleport(Vector3, Quaternion) instead.")] - [ClientRpc] - public void RpcTeleportAndRotate(Vector3 destination, Quaternion rotation) - { - OnTeleport(destination, rotation); - } - - // client->server teleport to force position without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [Command] - public void CmdTeleport(Vector3 destination) - { - // client can only teleport objects that it has authority over. - if (!clientAuthority) return; - - // TODO what about host mode? - OnTeleport(destination); - - // if a client teleports, we need to broadcast to everyone else too - // TODO the teleported client should ignore the rpc though. - // otherwise if it already moved again after teleporting, - // the rpc would come a little bit later and reset it once. - // TODO or not? if client ONLY calls Teleport(pos), the position - // would only be set after the rpc. unless the client calls - // BOTH Teleport(pos) and targetComponent.position=pos - RpcTeleport(destination); - } - - // client->server teleport to force position and rotation without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [Command] - public void CmdTeleport(Vector3 destination, Quaternion rotation) - { - // client can only teleport objects that it has authority over. - if (!clientAuthority) return; - - // TODO what about host mode? - OnTeleport(destination, rotation); - - // if a client teleports, we need to broadcast to everyone else too - // TODO the teleported client should ignore the rpc though. - // otherwise if it already moved again after teleporting, - // the rpc would come a little bit later and reset it once. - // TODO or not? if client ONLY calls Teleport(pos), the position - // would only be set after the rpc. unless the client calls - // BOTH Teleport(pos) and targetComponent.position=pos - RpcTeleport(destination, rotation); - } - - // Deprecated 2022-01-19 - [Obsolete("Use CmdTeleport(Vector3, Quaternion) instead.")] - [Command] - public void CmdTeleportAndRotate(Vector3 destination, Quaternion rotation) - { - if (!clientAuthority) return; - OnTeleport(destination, rotation); - RpcTeleport(destination, rotation); - } - - public virtual void Reset() - { - // disabled objects aren't updated anymore. - // so let's clear the buffers. - serverBuffer.Clear(); - clientBuffer.Clear(); - - // reset interpolation time too so we start at t=0 next time - serverInterpolationTime = 0; - clientInterpolationTime = 0; - } - - protected virtual void OnDisable() => Reset(); - protected virtual void OnEnable() => Reset(); - - protected virtual void OnValidate() - { - // make sure that catchup threshold is > buffer multiplier. - // for a buffer multiplier of '3', we usually have at _least_ 3 - // buffered snapshots. often 4-5 even. - // - // catchUpThreshold should be a minimum of bufferTimeMultiplier + 3, - // to prevent clashes with SnapshotInterpolation looking for at least - // 3 old enough buffers, else catch up will be implemented while there - // is not enough old buffers, and will result in jitter. - // (validated with several real world tests by ninja & imer) - catchupThreshold = Mathf.Max(bufferTimeMultiplier + 3, catchupThreshold); - - // buffer limit should be at least multiplier to have enough in there - bufferSizeLimit = Mathf.Max(bufferTimeMultiplier, bufferSizeLimit); - } - - public override bool OnSerialize(NetworkWriter writer, bool initialState) - { - // sync target component's position on spawn. - // fixes https://github.com/vis2k/Mirror/pull/3051/ - // (Spawn message wouldn't sync NTChild positions either) - if (initialState) - { - if (syncPosition) writer.WriteVector3(targetComponent.localPosition); - if (syncRotation) writer.WriteQuaternion(targetComponent.localRotation); - if (syncScale) writer.WriteVector3(targetComponent.localScale); - return true; - } - return false; - } - - public override void OnDeserialize(NetworkReader reader, bool initialState) - { - // sync target component's position on spawn. - // fixes https://github.com/vis2k/Mirror/pull/3051/ - // (Spawn message wouldn't sync NTChild positions either) - if (initialState) - { - if (syncPosition) targetComponent.localPosition = reader.ReadVector3(); - if (syncRotation) targetComponent.localRotation = reader.ReadQuaternion(); - if (syncScale) targetComponent.localScale = reader.ReadVector3(); - } - } - - // OnGUI allocates even if it does nothing. avoid in release. -#if UNITY_EDITOR || DEVELOPMENT_BUILD - // debug /////////////////////////////////////////////////////////////// - protected virtual void OnGUI() - { - if (!showOverlay) return; - - // show data next to player for easier debugging. this is very useful! - // IMPORTANT: this is basically an ESP hack for shooter games. - // DO NOT make this available with a hotkey in release builds - if (!Debug.isDebugBuild) return; - - // project position to screen - Vector3 point = Camera.main.WorldToScreenPoint(targetComponent.position); - - // enough alpha, in front of camera and in screen? - if (point.z >= 0 && Utils.IsPointInScreen(point)) - { - // catchup is useful to show too - int serverBufferExcess = Mathf.Max(serverBuffer.Count - catchupThreshold, 0); - int clientBufferExcess = Mathf.Max(clientBuffer.Count - catchupThreshold, 0); - float serverCatchup = serverBufferExcess * catchupMultiplier; - float clientCatchup = clientBufferExcess * catchupMultiplier; - - GUI.color = overlayColor; - GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100)); - - // always show both client & server buffers so it's super - // obvious if we accidentally populate both. - GUILayout.Label($"Server Buffer:{serverBuffer.Count}"); - if (serverCatchup > 0) - GUILayout.Label($"Server Catchup:{serverCatchup * 100:F2}%"); - - GUILayout.Label($"Client Buffer:{clientBuffer.Count}"); - if (clientCatchup > 0) - GUILayout.Label($"Client Catchup:{clientCatchup * 100:F2}%"); - - GUILayout.EndArea(); - GUI.color = Color.white; - } - } - - protected virtual void DrawGizmos(SortedList buffer) - { - // only draw if we have at least two entries - if (buffer.Count < 2) return; - - // calcluate threshold for 'old enough' snapshots - double threshold = NetworkTime.localTime - bufferTime; - Color oldEnoughColor = new Color(0, 1, 0, 0.5f); - Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); - - // draw the whole buffer for easier debugging. - // it's worth seeing how much we have buffered ahead already - for (int i = 0; i < buffer.Count; ++i) - { - // color depends on if old enough or not - NTSnapshot entry = buffer.Values[i]; - bool oldEnough = entry.localTimestamp <= threshold; - Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; - Gizmos.DrawCube(entry.position, Vector3.one); - } - - // extra: lines between start<->position<->goal - Gizmos.color = Color.green; - Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position); - Gizmos.color = Color.white; - Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position); - } - - protected virtual void OnDrawGizmos() - { - // This fires in edit mode but that spams NRE's so check isPlaying - if (!Application.isPlaying) return; - if (!showGizmos) return; - - if (isServer) DrawGizmos(serverBuffer); - if (isClient) DrawGizmos(clientBuffer); - } -#endif - } -} diff --git a/Assets/Mirror/Components/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransformBase.cs new file mode 100644 index 0000000..1cfe668 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransformBase.cs @@ -0,0 +1,385 @@ +// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ +// +// Base class for NetworkTransform and NetworkTransformChild. +// => simple unreliable sync without any interpolation for now. +// => which means we don't need teleport detection either +// +// NOTE: several functions are virtual in case someone needs to modify a part. +// +// Channel: uses UNRELIABLE at all times. +// -> out of order packets are dropped automatically +// -> it's better than RELIABLE for several reasons: +// * head of line blocking would add delay +// * resending is mostly pointless +// * bigger data race: +// -> if we use a Cmd() at position X over reliable +// -> client gets Cmd() and X at the same time, but buffers X for bufferTime +// -> for unreliable, it would get X before the reliable Cmd(), still +// buffer for bufferTime but end up closer to the original time +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public abstract class NetworkTransformBase : NetworkBehaviour + { + // target transform to sync. can be on a child. + [Header("Target")] + [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] + public Transform target; + + // TODO SyncDirection { ClientToServer, ServerToClient } is easier? + // Deprecated 2022-10-25 + [Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")] + [Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually. + [Tooltip("Obsolete: NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")] + public bool clientAuthority; + // Is this a client with authority over this transform? + // This component could be on the player object or any object that has been assigned authority to this client. + protected bool IsClientWithAuthority => isClient && authority; + public readonly SortedList clientSnapshots = new SortedList(); + public readonly SortedList serverSnapshots = new SortedList(); + + // selective sync ////////////////////////////////////////////////////// + [Header("Selective Sync & Interpolation\nDon't change these at Runtime")] + public bool syncPosition = true; // do not change at runtime! + public bool syncRotation = true; // do not change at runtime! + public bool syncScale = false; // do not change at runtime! rare. off by default. + + // debugging /////////////////////////////////////////////////////////// + [Header("Debug")] + public bool showGizmos; + public bool showOverlay; + public Color overlayColor = new Color(0, 0, 0, 0.5f); + + // initialization ////////////////////////////////////////////////////// + // make sure to call this when inheriting too! + protected virtual void Awake() {} + + protected virtual void OnValidate() + { + // set target to self if none yet + if (target == null) target = transform; + + // time snapshot interpolation happens globally. + // value (transform) happens in here. + // both always need to be on the same send interval. + // force the setting to '0' in OnValidate to make it obvious that we + // actually use NetworkServer.sendInterval. + syncInterval = 0; + + // obsolete clientAuthority compatibility: + // if it was used, then set the new SyncDirection automatically. + // if it wasn't used, then don't touch syncDirection. + #pragma warning disable CS0618 + if (clientAuthority) + { + syncDirection = SyncDirection.ClientToServer; + Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead."); + } + #pragma warning restore CS0618 + } + + // snapshot functions ////////////////////////////////////////////////// + // construct a snapshot of the current state + // => internal for testing + protected virtual TransformSnapshot Construct() + { + // NetworkTime.localTime for double precision until Unity has it too + return new TransformSnapshot( + // our local time is what the other end uses as remote time +#if !UNITY_2020_3_OR_NEWER + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet +#else + Time.timeAsDouble, +#endif + // the other end fills out local time itself + 0, + target.localPosition, + target.localRotation, + target.localScale + ); + } + + protected void AddSnapshot(SortedList snapshots, double timeStamp, Vector3? position, Quaternion? rotation, Vector3? scale) + { + // position, rotation, scale can have no value if same as last time. + // saves bandwidth. + // but we still need to feed it to snapshot interpolation. we can't + // just have gaps in there if nothing has changed. for example, if + // client sends snapshot at t=0 + // client sends nothing for 10s because not moved + // client sends snapshot at t=10 + // then the server would assume that it's one super slow move and + // replay it for 10 seconds. + if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : target.localPosition; + if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : target.localRotation; + if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale; + + // insert transform snapshot + SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot( + timeStamp, // arrival remote timestamp. NOT remote time. +#if !UNITY_2020_3_OR_NEWER + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet +#else + Time.timeAsDouble, +#endif + position.Value, + rotation.Value, + scale.Value + )); + } + + // apply a snapshot to the Transform. + // -> start, end, interpolated are all passed in caes they are needed + // -> a regular game would apply the 'interpolated' snapshot + // -> a board game might want to jump to 'goal' directly + // (it's easier to always interpolate and then apply selectively, + // instead of manually interpolating x, y, z, ... depending on flags) + // => internal for testing + // + // NOTE: stuck detection is unnecessary here. + // we always set transform.position anyway, we can't get stuck. + protected virtual void Apply(TransformSnapshot interpolated) + { + // local position/rotation for VR support + // + // if syncPosition/Rotation/Scale is disabled then we received nulls + // -> current position/rotation/scale would've been added as snapshot + // -> we still interpolated + // -> but simply don't apply it. if the user doesn't want to sync + // scale, then we should not touch scale etc. + if (syncPosition) target.localPosition = interpolated.position; + if (syncRotation) target.localRotation = interpolated.rotation; + if (syncScale) target.localScale = interpolated.scale; + } + + // client->server teleport to force position without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [Command] + public void CmdTeleport(Vector3 destination) + { + // client can only teleport objects that it has authority over. + if (syncDirection != SyncDirection.ClientToServer) return; + + // TODO what about host mode? + OnTeleport(destination); + + // if a client teleports, we need to broadcast to everyone else too + // TODO the teleported client should ignore the rpc though. + // otherwise if it already moved again after teleporting, + // the rpc would come a little bit later and reset it once. + // TODO or not? if client ONLY calls Teleport(pos), the position + // would only be set after the rpc. unless the client calls + // BOTH Teleport(pos) and target.position=pos + RpcTeleport(destination); + } + + // client->server teleport to force position and rotation without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [Command] + public void CmdTeleport(Vector3 destination, Quaternion rotation) + { + // client can only teleport objects that it has authority over. + if (syncDirection != SyncDirection.ClientToServer) return; + + // TODO what about host mode? + OnTeleport(destination, rotation); + + // if a client teleports, we need to broadcast to everyone else too + // TODO the teleported client should ignore the rpc though. + // otherwise if it already moved again after teleporting, + // the rpc would come a little bit later and reset it once. + // TODO or not? if client ONLY calls Teleport(pos), the position + // would only be set after the rpc. unless the client calls + // BOTH Teleport(pos) and target.position=pos + RpcTeleport(destination, rotation); + } + + // server->client teleport to force position without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [ClientRpc] + public void RpcTeleport(Vector3 destination) + { + // NOTE: even in client authority mode, the server is always allowed + // to teleport the player. for example: + // * CmdEnterPortal() might teleport the player + // * Some people use client authority with server sided checks + // so the server should be able to reset position if needed. + + // TODO what about host mode? + OnTeleport(destination); + } + + // server->client teleport to force position and rotation without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [ClientRpc] + public void RpcTeleport(Vector3 destination, Quaternion rotation) + { + // NOTE: even in client authority mode, the server is always allowed + // to teleport the player. for example: + // * CmdEnterPortal() might teleport the player + // * Some people use client authority with server sided checks + // so the server should be able to reset position if needed. + + // TODO what about host mode? + OnTeleport(destination, rotation); + } + + [ClientRpc] + void RpcReset() + { + Reset(); + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // reset any in-progress interpolation & buffers + Reset(); + + // set the new position. + // interpolation will automatically continue. + target.position = destination; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) + { + // reset any in-progress interpolation & buffers + Reset(); + + // set the new position. + // interpolation will automatically continue. + target.position = destination; + target.rotation = rotation; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + public virtual void Reset() + { + // disabled objects aren't updated anymore. + // so let's clear the buffers. + serverSnapshots.Clear(); + clientSnapshots.Clear(); + } + + protected virtual void OnEnable() + { + Reset(); + + if (NetworkServer.active) + NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged; + } + + protected virtual void OnDisable() + { + Reset(); + + if (NetworkServer.active) + NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged; + } + + [ServerCallback] + void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState) + { + if (identity != netIdentity) return; + + // If server gets authority or syncdirection is server to client, + // we don't reset buffers. + // This is because if syncdirection is S to C, we will never have + // snapshot issues since there is only ever 1 source. + + if (syncDirection == SyncDirection.ClientToServer) + { + Reset(); + RpcReset(); + } + } + + // OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // debug /////////////////////////////////////////////////////////////// + protected virtual void OnGUI() + { + if (!showOverlay) return; + if (!Camera.main) return; + + // show data next to player for easier debugging. this is very useful! + // IMPORTANT: this is basically an ESP hack for shooter games. + // DO NOT make this available with a hotkey in release builds + if (!Debug.isDebugBuild) return; + + // project position to screen + Vector3 point = Camera.main.WorldToScreenPoint(target.position); + + // enough alpha, in front of camera and in screen? + if (point.z >= 0 && Utils.IsPointInScreen(point)) + { + GUI.color = overlayColor; + GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100)); + + // always show both client & server buffers so it's super + // obvious if we accidentally populate both. + GUILayout.Label($"Server Buffer:{serverSnapshots.Count}"); + GUILayout.Label($"Client Buffer:{clientSnapshots.Count}"); + + GUILayout.EndArea(); + GUI.color = Color.white; + } + } + + protected virtual void DrawGizmos(SortedList buffer) + { + // only draw if we have at least two entries + if (buffer.Count < 2) return; + + // calculate threshold for 'old enough' snapshots + double threshold = NetworkTime.localTime - NetworkClient.bufferTime; + Color oldEnoughColor = new Color(0, 1, 0, 0.5f); + Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); + + // draw the whole buffer for easier debugging. + // it's worth seeing how much we have buffered ahead already + for (int i = 0; i < buffer.Count; ++i) + { + // color depends on if old enough or not + TransformSnapshot entry = buffer.Values[i]; + bool oldEnough = entry.localTime <= threshold; + Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; + Gizmos.DrawCube(entry.position, Vector3.one); + } + + // extra: lines between start<->position<->goal + Gizmos.color = Color.green; + Gizmos.DrawLine(buffer.Values[0].position, target.position); + Gizmos.color = Color.white; + Gizmos.DrawLine(target.position, buffer.Values[1].position); + } + + protected virtual void OnDrawGizmos() + { + // This fires in edit mode but that spams NRE's so check isPlaying + if (!Application.isPlaying) return; + if (!showGizmos) return; + + if (isServer) DrawGizmos(serverSnapshots); + if (isClient) DrawGizmos(clientSnapshots); + } +#endif + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs.meta b/Assets/Mirror/Components/NetworkTransformBase.cs.meta similarity index 83% rename from Assets/Mirror/Components/Experimental/NetworkTransformBase.cs.meta rename to Assets/Mirror/Components/NetworkTransformBase.cs.meta index d737bed..37a1147 100644 --- a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs.meta +++ b/Assets/Mirror/Components/NetworkTransformBase.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ea7c690c4fbf8c4439726f4c62eda6d3 +guid: 7c44135fde488424eaf28566206ce473 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Mirror/Components/NetworkTransformReliable.meta b/Assets/Mirror/Components/NetworkTransformReliable.meta new file mode 100644 index 0000000..1b6770d --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransformReliable.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 36de72d9255741659bcbd1971ed29822 +timeCreated: 1668358590 \ No newline at end of file diff --git a/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs new file mode 100644 index 0000000..718335b --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs @@ -0,0 +1,404 @@ +// NetworkTransform V3 (reliable) by mischa (2022-10) +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Reliable)")] + public class NetworkTransformReliable : NetworkTransformBase + { + [Header("Sync Only If Changed")] + [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] + public bool onlySyncOnChange = true; + [Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")] + public float onlySyncOnChangeCorrectionMultiplier = 2; + + [Header("Rotation")] + [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] + public float rotationSensitivity = 0.01f; + [Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")] + public bool compressRotation = false; + + // delta compression is capable of detecting byte-level changes. + // if we scale float position to bytes, + // then small movements will only change one byte. + // this gives optimal bandwidth. + // benchmark with 0.01 precision: 130 KB/s => 60 KB/s + // benchmark with 0.1 precision: 130 KB/s => 30 KB/s + [Header("Precision")] + [Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")] + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float positionPrecision = 0.01f; // 1 cm + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float scalePrecision = 0.01f; // 1 cm + + // delta compression needs to remember 'last' to compress against + protected Vector3Long lastSerializedPosition = Vector3Long.zero; + protected Vector3Long lastDeserializedPosition = Vector3Long.zero; + + protected Vector3Long lastSerializedScale = Vector3Long.zero; + protected Vector3Long lastDeserializedScale = Vector3Long.zero; + + // Used to store last sent snapshots + protected TransformSnapshot last; + + int lastClientCount = 0; + + // update ////////////////////////////////////////////////////////////// + void Update() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + + void UpdateServer() + { + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> don't apply for host mode player objects either, even if in + // client authority mode. if it doesn't go over the network, + // then we don't need to do anything. + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed); + } + } + + // set dirty to trigger OnSerialize. either always, or only if changed. + // technically snapshot interpolation requires constant sending. + // however, with reliable it should be fine without constant sends. + // + // detect changes _after_ all changes were applied above. + if (!onlySyncOnChange || Changed(Construct())) + SetDirty(); + } + + void UpdateClient() + { + // client authority, and local player (= allowed to move myself)? + if (IsClientWithAuthority) + { + // https://github.com/vis2k/Mirror/pull/2992/ + if (!NetworkClient.ready) return; + + // set dirty to trigger OnSerialize. either always, or only if changed. + // technically snapshot interpolation requires constant sending. + // however, with reliable it should be fine without constant sends. + if (!onlySyncOnChange || Changed(Construct())) + SetDirty(); + } + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer + else + { + + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed); + + } + + // 'only sync if moved' + // explain.. + // from 1 snap to next snap.. + // it'll be old... + if (lastClientCount > 1 && clientSnapshots.Count == 1) + { + // this is it. snapshots are down to '1'. + // does this cause stuck? + } + + lastClientCount = clientSnapshots.Count; + } + } + + // check if position / rotation / scale changed since last sync + protected virtual bool Changed(TransformSnapshot current) => + // position is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.position, current.position, positionPrecision) || + // rotation isn't quantized / delta compressed. + // check with sensitivity. + Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity || + // scale is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.scale, current.scale, scalePrecision); + + // helper function to compare quantized representations of a Vector3 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision) + { + Compression.ScaleToLong(u, precision, out Vector3Long uQuantized); + Compression.ScaleToLong(v, precision, out Vector3Long vQuantized); + return uQuantized != vQuantized; + } + + // NT may be used on client/server/host to Owner/Observers with + // ServerToClient or ClientToServer. + // however, OnSerialize should always delta against last. + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // get current snapshot for broadcasting. + TransformSnapshot snapshot = Construct(); + + // ClientToServer optimization: + // for interpolated client owned identities, + // always broadcast the latest known snapshot so other clients can + // interpolate immediately instead of catching up too + + // TODO dirty mask? [compression is very good w/o it already] + // each vector's component is delta compressed. + // an unchanged component would still require 1 byte. + // let's use a dirty bit mask to filter those out as well. + + // initial + if (initialState) + { + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + else + writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) writer.WriteVector3(snapshot.scale); + } + // delta + else + { + // int before = writer.Position; + + if (syncPosition) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedPosition, quantized); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + else + writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedScale, quantized); + } + + // int written = writer.Position - before; + // Debug.Log($"{name} compressed to {written} bytes"); + } + + // save serialized as 'last' for next delta compression + if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); + if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); + + // set 'last' + last = snapshot; + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + // initial + if (initialState) + { + if (syncPosition) position = reader.ReadVector3(); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + else + rotation = reader.ReadQuaternion(); + } + if (syncScale) scale = reader.ReadVector3(); + } + // delta + else + { + // varint -> delta -> quantize + if (syncPosition) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition); + position = Compression.ScaleToFloat(quantized, positionPrecision); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + else + rotation = reader.ReadQuaternion(); + } + if (syncScale) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale); + scale = Compression.ScaleToFloat(quantized, scalePrecision); + } + } + + // handle depending on server / client / host. + // server has priority for host mode. + if (isServer) OnClientToServerSync(position, rotation, scale); + else if (isClient) OnServerToClientSync(position, rotation, scale); + + // save deserialized as 'last' for next delta compression + if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); + if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); + } + + // sync //////////////////////////////////////////////////////////////// + + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; + + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + serverSnapshots, + connectionToClient.remoteTimeStamp, + NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline. + NetworkServer.sendInterval, // Unity 2019 doesn't have timeAsDouble yet + target.localPosition, + target.localRotation, + target.localScale); + // Debug.Log($"{name}: corrected history on server to fix initial stutter after not sending for a while."); + } + + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp, position, rotation, scale); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + clientSnapshots, + NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + NetworkClient.sendInterval, + target.localPosition, + target.localRotation, + target.localScale); + // Debug.Log($"{name}: corrected history on client to fix initial stutter after not sending for a while."); + } + + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp, position, rotation, scale); + } + + // only sync on change ///////////////////////////////////////////////// + // snap interp. needs a continous flow of packets. + // 'only sync on change' interrupts it while not changed. + // once it restarts, snap interp. will interp from the last old position. + // this will cause very noticeable stutter for the first move each time. + // the fix is quite simple. + + // 1. detect if the remaining snapshot is too old from a past move. + static bool NeedsCorrection( + SortedList snapshots, + double remoteTimestamp, + double bufferTime, + double toleranceMultiplier) => + snapshots.Count == 1 && + remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier; + + // 2. insert a fake snapshot at current position, + // exactly one 'sendInterval' behind the newly received one. + static void RewriteHistory( + SortedList snapshots, + // timestamp of packet arrival, not interpolated remote time! + double remoteTimeStamp, + double localTime, + double sendInterval, + Vector3 position, + Quaternion rotation, + Vector3 scale) + { + // clear the previous snapshot + snapshots.Clear(); + + // insert a fake one at where we used to be, + // 'sendInterval' behind the new one. + SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot( + remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time. + localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet + position, + rotation, + scale + )); + } + + public override void Reset() + { + base.Reset(); + + // reset delta + lastSerializedPosition = Vector3Long.zero; + lastDeserializedPosition = Vector3Long.zero; + + lastSerializedScale = Vector3Long.zero; + lastDeserializedScale = Vector3Long.zero; + } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs.meta b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs.meta similarity index 86% rename from Assets/Mirror/Components/Experimental/NetworkTransformChild.cs.meta rename to Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs.meta index 30f0d89..ece9c7d 100644 --- a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs.meta +++ b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f65214da13a861f4a8ae309d3daea1c6 +guid: 8ff3ba0becae47b8b9381191598957c8 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Mirror/Components/NetworkTransform2k.meta b/Assets/Mirror/Components/NetworkTransformUnreliable.meta similarity index 100% rename from Assets/Mirror/Components/NetworkTransform2k.meta rename to Assets/Mirror/Components/NetworkTransformUnreliable.meta diff --git a/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs new file mode 100644 index 0000000..35a9154 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs @@ -0,0 +1,359 @@ +// NetworkTransform V2 by mischa (2021-07) +// comment out the below line to quickly revert the onlySyncOnChange feature +#define onlySyncOnChange_BANDWIDTH_SAVING +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Unreliable)")] + public class NetworkTransform : NetworkTransformBase + { + // only sync when changed hack ///////////////////////////////////////// +#if onlySyncOnChange_BANDWIDTH_SAVING + [Header("Sync Only If Changed")] + [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] + public bool onlySyncOnChange = true; + + // 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching. + [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")] + public float bufferResetMultiplier = 5; + + [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] + public float positionSensitivity = 0.01f; + public float rotationSensitivity = 0.01f; + public float scaleSensitivity = 0.01f; + + protected bool positionChanged; + protected bool rotationChanged; + protected bool scaleChanged; + + // Used to store last sent snapshots + protected TransformSnapshot lastSnapshot; + protected bool cachedSnapshotComparison; + protected bool hasSentUnchangedPosition; +#endif + + double lastClientSendTime; + double lastServerSendTime; + + // update ////////////////////////////////////////////////////////////// + void Update() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + + void UpdateServer() + { + // broadcast to all clients each 'sendInterval' + // (client with authority will drop the rpc) + // NetworkTime.localTime for double precision until Unity has it too + // + // IMPORTANT: + // snapshot interpolation requires constant sending. + // DO NOT only send if position changed. for example: + // --- + // * client sends first position at t=0 + // * ... 10s later ... + // * client moves again, sends second position at t=10 + // --- + // * server gets first position at t=0 + // * server gets second position at t=10 + // * server moves from first to second within a time of 10s + // => would be a super slow move, instead of a wait & move. + // + // IMPORTANT: + // DO NOT send nulls if not changed 'since last send' either. we + // send unreliable and don't know which 'last send' the other end + // received successfully. + // + // Checks to ensure server only sends snapshots if object is + // on server authority(!clientAuthority) mode because on client + // authority mode snapshots are broadcasted right after the authoritative + // client updates server in the command function(see above), OR, + // since host does not send anything to update the server, any client + // authoritative movement done by the host will have to be broadcasted + // here by checking IsClientWithAuthority. + // TODO send same time that NetworkServer sends time snapshot? + if (NetworkTime.localTime >= lastServerSendTime + NetworkServer.sendInterval && // same interval as time interpolation! + (syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority)) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + TransformSnapshot snapshot = Construct(); +#if onlySyncOnChange_BANDWIDTH_SAVING + cachedSnapshotComparison = CompareSnapshots(snapshot); + if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } +#endif + +#if onlySyncOnChange_BANDWIDTH_SAVING + RpcServerToClientSync( + // only sync what the user wants to sync + syncPosition && positionChanged ? snapshot.position : default(Vector3?), + syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), + syncScale && scaleChanged ? snapshot.scale : default(Vector3?) + ); +#else + RpcServerToClientSync( + // only sync what the user wants to sync + syncPosition ? snapshot.position : default(Vector3?), + syncRotation ? snapshot.rotation : default(Quaternion?), + syncScale ? snapshot.scale : default(Vector3?) + ); +#endif + + lastServerSendTime = NetworkTime.localTime; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (cachedSnapshotComparison) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + lastSnapshot = snapshot; + } +#endif + } + + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> don't apply for host mode player objects either, even if in + // client authority mode. if it doesn't go over the network, + // then we don't need to do anything. + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed); + } + } + } + + void UpdateClient() + { + // client authority, and local player (= allowed to move myself)? + if (IsClientWithAuthority) + { + // https://github.com/vis2k/Mirror/pull/2992/ + if (!NetworkClient.ready) return; + + // send to server each 'sendInterval' + // NetworkTime.localTime for double precision until Unity has it too + // + // IMPORTANT: + // snapshot interpolation requires constant sending. + // DO NOT only send if position changed. for example: + // --- + // * client sends first position at t=0 + // * ... 10s later ... + // * client moves again, sends second position at t=10 + // --- + // * server gets first position at t=0 + // * server gets second position at t=10 + // * server moves from first to second within a time of 10s + // => would be a super slow move, instead of a wait & move. + // + // IMPORTANT: + // DO NOT send nulls if not changed 'since last send' either. we + // send unreliable and don't know which 'last send' the other end + // received successfully. + if (NetworkTime.localTime >= lastClientSendTime + NetworkClient.sendInterval) // same interval as time interpolation! + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + TransformSnapshot snapshot = Construct(); +#if onlySyncOnChange_BANDWIDTH_SAVING + cachedSnapshotComparison = CompareSnapshots(snapshot); + if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } +#endif + +#if onlySyncOnChange_BANDWIDTH_SAVING + CmdClientToServerSync( + // only sync what the user wants to sync + syncPosition && positionChanged ? snapshot.position : default(Vector3?), + syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), + syncScale && scaleChanged ? snapshot.scale : default(Vector3?) + ); +#else + CmdClientToServerSync( + // only sync what the user wants to sync + syncPosition ? snapshot.position : default(Vector3?), + syncRotation ? snapshot.rotation : default(Quaternion?), + syncScale ? snapshot.scale : default(Vector3?) + ); +#endif + + lastClientSendTime = NetworkTime.localTime; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (cachedSnapshotComparison) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + lastSnapshot = snapshot; + } +#endif + } + } + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer + else + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed); + } + } + } + + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // sync target component's position on spawn. + // fixes https://github.com/vis2k/Mirror/pull/3051/ + // (Spawn message wouldn't sync NTChild positions either) + if (initialState) + { + if (syncPosition) writer.WriteVector3(target.localPosition); + if (syncRotation) writer.WriteQuaternion(target.localRotation); + if (syncScale) writer.WriteVector3(target.localScale); + } + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // sync target component's position on spawn. + // fixes https://github.com/vis2k/Mirror/pull/3051/ + // (Spawn message wouldn't sync NTChild positions either) + if (initialState) + { + if (syncPosition) target.localPosition = reader.ReadVector3(); + if (syncRotation) target.localRotation = reader.ReadQuaternion(); + if (syncScale) target.localScale = reader.ReadVector3(); + } + } + +#if onlySyncOnChange_BANDWIDTH_SAVING + // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. + protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot) + { + positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; + rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; + scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity; + + return (!positionChanged && !rotationChanged && !scaleChanged); + } +#endif + // cmd ///////////////////////////////////////////////////////////////// + // only unreliable. see comment above of this file. + [Command(channel = Channels.Unreliable)] + void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + OnClientToServerSync(position, rotation, scale); + //For client authority, immediately pass on the client snapshot to all other + //clients instead of waiting for server to send its snapshots. + if (syncDirection == SyncDirection.ClientToServer) + { + RpcServerToClientSync(position, rotation, scale); + } + } + + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; + + // only player owned objects (with a connection) can send to + // server. we can get the timestamp from the connection. + double timestamp = connectionToClient.remoteTimeStamp; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (onlySyncOnChange) + { + double timeIntervalCheck = bufferResetMultiplier * NetworkClient.sendInterval; + + if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + { + Reset(); + } + } +#endif + AddSnapshot(serverSnapshots, timestamp, position, rotation, scale); + } + + // rpc ///////////////////////////////////////////////////////////////// + // only unreliable. see comment above of this file. + [ClientRpc(channel = Channels.Unreliable)] + void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) => + OnServerToClientSync(position, rotation, scale); + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // in host mode, the server sends rpcs to all clients. + // the host client itself will receive them too. + // -> host server is always the source of truth + // -> we can ignore any rpc on the host client + // => otherwise host objects would have ever growing clientBuffers + // (rpc goes to clients. if isServer is true too then we are host) + if (isServer) return; + + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // on the client, we receive rpcs for all entities. + // not all of them have a connectionToServer. + // but all of them go through NetworkClient.connection. + // we can get the timestamp from there. + double timestamp = NetworkClient.connection.remoteTimeStamp; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (onlySyncOnChange) + { + double timeIntervalCheck = bufferResetMultiplier * NetworkServer.sendInterval; + + if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + { + Reset(); + } + } +#endif + AddSnapshot(clientSnapshots, timestamp, position, rotation, scale); + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs.meta similarity index 100% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta rename to Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs.meta diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs similarity index 54% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs rename to Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs index 8032506..a844d9d 100644 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs +++ b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs @@ -1,14 +1,12 @@ // A component to synchronize the position of child transforms of networked objects. // There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values. +using System; using UnityEngine; namespace Mirror { - [AddComponentMenu("Network/Network Transform Child")] - public class NetworkTransformChild : NetworkTransformBase - { - [Header("Target")] - public Transform target; - protected override Transform targetComponent => target; - } + // Deprecated 2022-10-25 + [AddComponentMenu("")] + [Obsolete("NetworkTransformChild is not needed anymore. The .target is now exposed in NetworkTransform itself. Note you can open the Inspector in debug view and replace the source script instead of reassigning everything.")] + public class NetworkTransformChild : NetworkTransform {} } diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs.meta similarity index 100% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta rename to Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs.meta diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs b/Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs similarity index 80% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs rename to Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs index efd91c0..912b10d 100644 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs +++ b/Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs @@ -6,7 +6,7 @@ namespace Mirror { // NetworkTransform Snapshot - public struct NTSnapshot : Snapshot + public struct TransformSnapshot : Snapshot { // time or sequence are needed to throw away older snapshots. // @@ -23,30 +23,31 @@ public struct NTSnapshot : Snapshot // // [REMOTE TIME, NOT LOCAL TIME] // => DOUBLE for long term accuracy & batching gives us double anyway - public double remoteTimestamp { get; set; } + public double remoteTime { get; set; } + // the local timestamp (when we received it) // used to know if the first two snapshots are old enough to start. - public double localTimestamp { get; set; } + public double localTime { get; set; } - public Vector3 position; + public Vector3 position; public Quaternion rotation; - public Vector3 scale; + public Vector3 scale; - public NTSnapshot(double remoteTimestamp, double localTimestamp, Vector3 position, Quaternion rotation, Vector3 scale) + public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale) { - this.remoteTimestamp = remoteTimestamp; - this.localTimestamp = localTimestamp; + this.remoteTime = remoteTime; + this.localTime = localTime; this.position = position; this.rotation = rotation; this.scale = scale; } - public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t) + public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t) { // NOTE: // Vector3 & Quaternion components are float anyway, so we can // keep using the functions with 't' as float instead of double. - return new NTSnapshot( + return new TransformSnapshot( // interpolated snapshot is applied directly. don't need timestamps. 0, 0, // lerp position/rotation/scale unclamped in case we ever need diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta b/Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs.meta similarity index 100% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta rename to Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs.meta diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs new file mode 100644 index 0000000..6e872b1 --- /dev/null +++ b/Assets/Mirror/Components/RemoteStatistics.cs @@ -0,0 +1,439 @@ +// remote statistics panel from Mirror II to show connections, load, etc. +// server syncs statistics to clients if authenticated. +// +// attach this to a player. +// requires NetworkStatistics component on the Network object. +// +// Unity's OnGUI is the easiest to use solution at the moment. +// * playfab is super complex to set up +// * http servers would be nice, but still need to open ports, live refresh, etc +// +// for safety reasons, let's keep this read-only. +// at least until there's safe authentication. +using System; +using System.IO; +using UnityEngine; + +namespace Mirror +{ + // server -> client + struct Stats + { + // general + public int connections; + public double uptime; + public int configuredTickRate; + public int actualTickRate; + + // traffic + public long sentBytesPerSecond; + public long receiveBytesPerSecond; + + // cpu + public float serverTickInterval; + public double fullUpdateAvg; + public double serverEarlyAvg; + public double serverLateAvg; + public double transportEarlyAvg; + public double transportLateAvg; + + // C# boilerplate + public Stats( + // general + int connections, + double uptime, + int configuredTickRate, + int actualTickRate, + // traffic + long sentBytesPerSecond, + long receiveBytesPerSecond, + // cpu + float serverTickInterval, + double fullUpdateAvg, + double serverEarlyAvg, + double serverLateAvg, + double transportEarlyAvg, + double transportLateAvg + ) + { + // general + this.connections = connections; + this.uptime = uptime; + this.configuredTickRate = configuredTickRate; + this.actualTickRate = actualTickRate; + + // traffic + this.sentBytesPerSecond = sentBytesPerSecond; + this.receiveBytesPerSecond = receiveBytesPerSecond; + + // cpu + this.serverTickInterval = serverTickInterval; + this.fullUpdateAvg = fullUpdateAvg; + this.serverEarlyAvg = serverEarlyAvg; + this.serverLateAvg = serverLateAvg; + this.transportEarlyAvg = transportEarlyAvg; + this.transportLateAvg = transportLateAvg; + } + } + + // [RequireComponent(typeof(NetworkStatistics))] <- needs to be on Network GO, not on NI + public class RemoteStatistics : NetworkBehaviour + { + // components ("fake statics" for similar API) + protected NetworkStatistics NetworkStatistics; + + // broadcast to client. + // stats are quite huge, let's only send every few seconds via TargetRpc. + // instead of sending multiple times per second via NB.OnSerialize. + [Tooltip("Send stats every 'interval' seconds to client.")] + public float sendInterval = 1; + double lastSendTime; + + [Header("GUI")] + public bool showGui; + public KeyCode hotKey = KeyCode.F11; + Rect windowRect = new Rect(0, 0, 400, 400); + + // password can't be stored in code or in Unity project. + // it would be available in clients otherwise. + // this is not perfectly secure. that's why RemoteStatistics is read-only. + [Header("Authentication")] + public string passwordFile = "remote_statistics.txt"; + protected bool serverAuthenticated; // client needs to authenticate + protected bool clientAuthenticated; // show GUI until authenticated + protected string serverPassword = null; // null means not found, auth impossible + protected string clientPassword = ""; // for GUI + + // statistics synced to client + Stats stats; + + void LoadPassword() + { + // TODO only load once, not for all players? + // let's avoid static state for now. + + // load the password + string path = Path.GetFullPath(passwordFile); + if (File.Exists(path)) + { + // don't spam the server logs for every player's loaded file + // Debug.Log($"RemoteStatistics: loading password file: {path}"); + try + { + serverPassword = File.ReadAllText(path); + } + catch (Exception exception) + { + Debug.LogWarning($"RemoteStatistics: failed to read password file: {exception}"); + } + } + else + { + Debug.LogWarning($"RemoteStatistics: password file has not been created. Authentication will be impossible. Please save the password in: {path}"); + } + } + + void OnValidate() + { + syncMode = SyncMode.Owner; + } + + // make sure to call base function when overwriting! + // public so it can also be called from tests (and be overwritten by users) + public override void OnStartServer() + { + NetworkStatistics = NetworkManager.singleton.GetComponent(); + if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!"); + + // server needs to load the password + LoadPassword(); + } + + public override void OnStartLocalPlayer() + { + // center the window initially + windowRect.x = Screen.width / 2 - windowRect.width / 2; + windowRect.y = Screen.height / 2 - windowRect.height / 2; + } + + [TargetRpc] + void TargetRpcSync(Stats v) + { + // store stats and flag as authenticated + clientAuthenticated = true; + stats = v; + } + + [Command] + public void CmdAuthenticate(string v) + { + // was a valid password loaded on the server, + // and did the client send the correct one? + if (!string.IsNullOrWhiteSpace(serverPassword) && + serverPassword.Equals(v)) + { + serverAuthenticated = true; + Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}"); + } + } + + void UpdateServer() + { + // only sync if client has authenticated on the server + if (!serverAuthenticated) return; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= lastSendTime + sendInterval) + { + lastSendTime = NetworkTime.localTime; + + // target rpc to owner client + TargetRpcSync(new Stats( + // general + NetworkServer.connections.Count, + NetworkTime.time, + NetworkServer.tickRate, + NetworkServer.actualTickRate, + + // traffic + NetworkStatistics.serverSentBytesPerSecond, + NetworkStatistics.serverReceivedBytesPerSecond, + + // cpu + NetworkServer.tickInterval, + NetworkServer.fullUpdateDuration.average, + NetworkServer.earlyUpdateDuration.average, + NetworkServer.lateUpdateDuration.average, + 0, // TODO ServerTransport.earlyUpdateDuration.average, + 0 // TODO ServerTransport.lateUpdateDuration.average + )); + } + } + void UpdateClient() + { + if (Input.GetKeyDown(hotKey)) + showGui = !showGui; + } + + void Update() + { + if (isServer) UpdateServer(); + if (isLocalPlayer) UpdateClient(); + } + + void OnGUI() + { + if (!isLocalPlayer) return; + if (!showGui) return; + + windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics"); + windowRect = Utils.KeepInScreen(windowRect); + } + + // Text: value + void GUILayout_TextAndValue(string text, string value) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(text); + GUILayout.FlexibleSpace(); + GUILayout.Label(value); + GUILayout.EndHorizontal(); + } + + // fake a progress bar via horizontal scroll bar with ratio as width + void GUILayout_ProgressBar(double ratio, int width) + { + // clamp ratio, otherwise >1 would make it extremely large + ratio = Mathd.Clamp01(ratio); + GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width)); + } + + // need to specify progress bar & caption width, + // otherwise differently sized captions would always misalign the + // progress bars. + void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(text); + GUILayout.FlexibleSpace(); + GUILayout_ProgressBar(ratio, progressbarWidth); + + // coloring the caption is enough. otherwise it's too much. + GUI.color = captionColor; + GUILayout.Label(caption, GUILayout.Width(captionWidth)); + GUI.color = Color.white; + + GUILayout.EndHorizontal(); + } + + void GUI_Authenticate() + { + GUILayout.BeginVertical("Box"); // start general + GUILayout.Label("Authentication"); + + // warning if insecure connection + // if (ClientTransport.IsEncrypted()) + // { + // GUILayout.Label("Connection is encrypted!"); + // } + // else + // { + GUILayout.Label("Connection is not encrypted. Use with care!"); + // } + + // input + clientPassword = GUILayout.PasswordField(clientPassword, '*'); + + // button + GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword); + if (GUILayout.Button("Authenticate")) + { + CmdAuthenticate(clientPassword); + } + GUI.enabled = true; + + GUILayout.EndVertical(); // end general + } + + void GUI_General( + int connections, + double uptime, + int configuredTickRate, + int actualTickRate) + { + GUILayout.BeginVertical("Box"); // start general + GUILayout.Label("General"); + + // connections + GUILayout_TextAndValue("Connections:", $"{connections}"); + + // uptime + GUILayout_TextAndValue("Uptime:", $"{Utils.PrettySeconds(uptime)}"); // TODO + + // tick rate + // might be lower under heavy load. + // might be higher in editor if targetFrameRate can't be set. + GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green; + GUILayout_TextAndValue("Tick Rate:", $"{actualTickRate} Hz / {configuredTickRate} Hz"); + GUI.color = Color.white; + + GUILayout.EndVertical(); // end general + } + + void GUI_Traffic( + long serverSentBytesPerSecond, + long serverReceivedBytesPerSecond) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("Network"); + + GUILayout_TextAndValue("Outgoing:", $"{Utils.PrettyBytes(serverSentBytesPerSecond) }/s"); + GUILayout_TextAndValue("Incoming:", $"{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s"); + + GUILayout.EndVertical(); + } + + void GUI_Cpu( + float serverTickInterval, + double fullUpdateAvg, + double serverEarlyAvg, + double serverLateAvg, + double transportEarlyAvg, + double transportLateAvg) + { + const int barWidth = 120; + const int captionWidth = 90; + + GUILayout.BeginVertical("Box"); + GUILayout.Label("CPU"); + + // unity update + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + double fullRatio = fullUpdateAvg / serverTickInterval; + GUILayout_TextAndProgressBar( + "World Update Avg:", + fullRatio, + barWidth, $"{fullUpdateAvg * 1000:F1} ms", + captionWidth, + fullRatio <= 0.9 ? Color.green : Color.red); + + // server update + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval; + GUILayout_TextAndProgressBar( + "Server Update Avg:", + serverRatio, + barWidth, $"{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms", + captionWidth, + serverRatio <= 0.9 ? Color.green : Color.red); + + // transport: early + late update milliseconds. + // for threaded transport, this is the thread's update time. + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + // double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval; + // GUILayout_TextAndProgressBar( + // "Transport Avg:", + // transportRatio, + // barWidth, + // $"{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms", + // captionWidth, + // transportRatio <= 0.9 ? Color.green : Color.red); + + GUILayout.EndVertical(); + } + + void GUI_Notice() + { + // for security reasons, let's keep this read-only for now. + + // single line keeps input & visuals simple + // GUILayout.BeginVertical("Box"); + // GUILayout.Label("Global Notice"); + // notice = GUILayout.TextField(notice); + // if (GUILayout.Button("Send")) + // { + // // TODO + // } + // GUILayout.EndVertical(); + } + + void OnWindow(int windowID) + { + if (!clientAuthenticated) + { + GUI_Authenticate(); + } + else + { + GUI_General( + stats.connections, + stats.uptime, + stats.configuredTickRate, + stats.actualTickRate + ); + + GUI_Traffic( + stats.sentBytesPerSecond, + stats.receiveBytesPerSecond + ); + + GUI_Cpu( + stats.serverTickInterval, + stats.fullUpdateAvg, + stats.serverEarlyAvg, + stats.serverLateAvg, + stats.transportEarlyAvg, + stats.transportLateAvg + ); + + GUI_Notice(); + } + + // dragable window in any case + GUI.DragWindow(new Rect(0, 0, 10000, 10000)); + } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkTransform.cs.meta b/Assets/Mirror/Components/RemoteStatistics.cs.meta similarity index 86% rename from Assets/Mirror/Components/Experimental/NetworkTransform.cs.meta rename to Assets/Mirror/Components/RemoteStatistics.cs.meta index 2bc16dd..4c4d043 100644 --- a/Assets/Mirror/Components/Experimental/NetworkTransform.cs.meta +++ b/Assets/Mirror/Components/RemoteStatistics.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 741bbe11f5357b44593b15c0d11b16bd +guid: ba360e4ff6b44fc6898f56322b90c6c8 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Mirror/Runtime.meta b/Assets/Mirror/Core.meta similarity index 100% rename from Assets/Mirror/Runtime.meta rename to Assets/Mirror/Core.meta diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs b/Assets/Mirror/Core/AssemblyInfo.cs similarity index 100% rename from Assets/Mirror/Runtime/AssemblyInfo.cs rename to Assets/Mirror/Core/AssemblyInfo.cs diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs.meta b/Assets/Mirror/Core/AssemblyInfo.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/AssemblyInfo.cs.meta rename to Assets/Mirror/Core/AssemblyInfo.cs.meta diff --git a/Assets/Mirror/Runtime/Attributes.cs b/Assets/Mirror/Core/Attributes.cs similarity index 100% rename from Assets/Mirror/Runtime/Attributes.cs rename to Assets/Mirror/Core/Attributes.cs diff --git a/Assets/Mirror/Runtime/Attributes.cs.meta b/Assets/Mirror/Core/Attributes.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Attributes.cs.meta rename to Assets/Mirror/Core/Attributes.cs.meta diff --git a/Assets/Mirror/Runtime/Batching.meta b/Assets/Mirror/Core/Batching.meta similarity index 100% rename from Assets/Mirror/Runtime/Batching.meta rename to Assets/Mirror/Core/Batching.meta diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs b/Assets/Mirror/Core/Batching/Batcher.cs similarity index 100% rename from Assets/Mirror/Runtime/Batching/Batcher.cs rename to Assets/Mirror/Core/Batching/Batcher.cs diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs.meta b/Assets/Mirror/Core/Batching/Batcher.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Batching/Batcher.cs.meta rename to Assets/Mirror/Core/Batching/Batcher.cs.meta diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs b/Assets/Mirror/Core/Batching/Unbatcher.cs similarity index 99% rename from Assets/Mirror/Runtime/Batching/Unbatcher.cs rename to Assets/Mirror/Core/Batching/Unbatcher.cs index 495ada9..997b54a 100644 --- a/Assets/Mirror/Runtime/Batching/Unbatcher.cs +++ b/Assets/Mirror/Core/Batching/Unbatcher.cs @@ -101,7 +101,7 @@ public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp } // was our reader pointed to anything yet? - if (reader.Length == 0) + if (reader.Capacity == 0) { remoteTimeStamp = 0; return false; diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta b/Assets/Mirror/Core/Batching/Unbatcher.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta rename to Assets/Mirror/Core/Batching/Unbatcher.cs.meta diff --git a/Assets/Mirror/Runtime/Empty.meta b/Assets/Mirror/Core/Empty.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty.meta rename to Assets/Mirror/Core/Empty.meta diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs b/Assets/Mirror/Core/Empty/ClientScene.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/ClientScene.cs rename to Assets/Mirror/Core/Empty/ClientScene.cs diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta b/Assets/Mirror/Core/Empty/ClientScene.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/ClientScene.cs.meta rename to Assets/Mirror/Core/Empty/ClientScene.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud.meta b/Assets/Mirror/Core/Empty/Cloud.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud.meta rename to Assets/Mirror/Core/Empty/Cloud.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs b/Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs rename to Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs b/Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs rename to Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs b/Assets/Mirror/Core/Empty/Cloud/Ball.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Ball.cs rename to Assets/Mirror/Core/Empty/Cloud/Ball.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Ball.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/Ball.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs b/Assets/Mirror/Core/Empty/Cloud/BallManager.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs rename to Assets/Mirror/Core/Empty/Cloud/BallManager.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta b/Assets/Mirror/Core/Empty/Cloud/BallManager.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/BallManager.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs b/Assets/Mirror/Core/Empty/Cloud/BaseApi.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs rename to Assets/Mirror/Core/Empty/Cloud/BaseApi.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/BaseApi.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/BaseApi.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs b/Assets/Mirror/Core/Empty/Cloud/Events.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Events.cs rename to Assets/Mirror/Core/Empty/Cloud/Events.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Events.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/Events.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs b/Assets/Mirror/Core/Empty/Cloud/Extensions.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs rename to Assets/Mirror/Core/Empty/Cloud/Extensions.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Extensions.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/Extensions.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs b/Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs rename to Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs b/Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs rename to Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta b/Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs b/Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs rename to Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta b/Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs b/Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs rename to Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta b/Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs b/Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs rename to Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta b/Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs b/Assets/Mirror/Core/Empty/Cloud/ListServer.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs rename to Assets/Mirror/Core/Empty/Cloud/ListServer.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServer.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ListServer.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs rename to Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs rename to Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs rename to Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs rename to Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs b/Assets/Mirror/Core/Empty/Cloud/Logger.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Logger.cs rename to Assets/Mirror/Core/Empty/Cloud/Logger.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Logger.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/Logger.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs b/Assets/Mirror/Core/Empty/Cloud/Player.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Player.cs rename to Assets/Mirror/Core/Empty/Cloud/Player.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Player.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/Player.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs b/Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs rename to Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta b/Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs b/Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs rename to Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta b/Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs b/Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs rename to Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta b/Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs b/Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs rename to Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs b/Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs rename to Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs b/Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs rename to Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta rename to Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs b/Assets/Mirror/Core/Empty/DotNetCompatibility.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs rename to Assets/Mirror/Core/Empty/DotNetCompatibility.cs diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta b/Assets/Mirror/Core/Empty/DotNetCompatibility.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta rename to Assets/Mirror/Core/Empty/DotNetCompatibility.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs b/Assets/Mirror/Core/Empty/FallbackTransport.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/FallbackTransport.cs rename to Assets/Mirror/Core/Empty/FallbackTransport.cs diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta b/Assets/Mirror/Core/Empty/FallbackTransport.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta rename to Assets/Mirror/Core/Empty/FallbackTransport.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs b/Assets/Mirror/Core/Empty/LogFactory.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/LogFactory.cs rename to Assets/Mirror/Core/Empty/LogFactory.cs diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta b/Assets/Mirror/Core/Empty/LogFactory.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/LogFactory.cs.meta rename to Assets/Mirror/Core/Empty/LogFactory.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs b/Assets/Mirror/Core/Empty/LogFilter.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/LogFilter.cs rename to Assets/Mirror/Core/Empty/LogFilter.cs diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta b/Assets/Mirror/Core/Empty/LogFilter.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/LogFilter.cs.meta rename to Assets/Mirror/Core/Empty/LogFilter.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging.meta b/Assets/Mirror/Core/Empty/Logging.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging.meta rename to Assets/Mirror/Core/Empty/Logging.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs b/Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs rename to Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta b/Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta rename to Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs b/Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs rename to Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta b/Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta rename to Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs b/Assets/Mirror/Core/Empty/Logging/LogFactory.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs rename to Assets/Mirror/Core/Empty/Logging/LogFactory.cs diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta b/Assets/Mirror/Core/Empty/Logging/LogFactory.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta rename to Assets/Mirror/Core/Empty/Logging/LogFactory.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs b/Assets/Mirror/Core/Empty/Logging/LogSettings.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs rename to Assets/Mirror/Core/Empty/Logging/LogSettings.cs diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta b/Assets/Mirror/Core/Empty/Logging/LogSettings.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta rename to Assets/Mirror/Core/Empty/Logging/LogSettings.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs b/Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs rename to Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta b/Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta rename to Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs b/Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs rename to Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta b/Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta rename to Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs b/Assets/Mirror/Core/Empty/NetworkMatchChecker.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs rename to Assets/Mirror/Core/Empty/NetworkMatchChecker.cs diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkMatchChecker.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta rename to Assets/Mirror/Core/Empty/NetworkMatchChecker.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs b/Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs rename to Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta rename to Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs b/Assets/Mirror/Core/Empty/NetworkProximityChecker.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs rename to Assets/Mirror/Core/Empty/NetworkProximityChecker.cs diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkProximityChecker.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta rename to Assets/Mirror/Core/Empty/NetworkProximityChecker.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs b/Assets/Mirror/Core/Empty/NetworkSceneChecker.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs rename to Assets/Mirror/Core/Empty/NetworkSceneChecker.cs diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkSceneChecker.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta rename to Assets/Mirror/Core/Empty/NetworkSceneChecker.cs.meta diff --git a/Assets/Mirror/Core/Empty/NetworkTransformBase.cs b/Assets/Mirror/Core/Empty/NetworkTransformBase.cs new file mode 100644 index 0000000..79e858f --- /dev/null +++ b/Assets/Mirror/Core/Empty/NetworkTransformBase.cs @@ -0,0 +1 @@ +// removed 2022-10-24 diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta b/Assets/Mirror/Core/Empty/NetworkTransformBase.cs.meta similarity index 100% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta rename to Assets/Mirror/Core/Empty/NetworkTransformBase.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs b/Assets/Mirror/Core/Empty/NetworkVisibility.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkVisibility.cs rename to Assets/Mirror/Core/Empty/NetworkVisibility.cs diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta b/Assets/Mirror/Core/Empty/NetworkVisibility.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta rename to Assets/Mirror/Core/Empty/NetworkVisibility.cs.meta diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs b/Assets/Mirror/Core/Empty/StringHash.cs similarity index 100% rename from Assets/Mirror/Runtime/Empty/StringHash.cs rename to Assets/Mirror/Core/Empty/StringHash.cs diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta b/Assets/Mirror/Core/Empty/StringHash.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Empty/StringHash.cs.meta rename to Assets/Mirror/Core/Empty/StringHash.cs.meta diff --git a/Assets/Mirror/Core/Empty/SyncVar.cs b/Assets/Mirror/Core/Empty/SyncVar.cs new file mode 100644 index 0000000..aaa3b9d --- /dev/null +++ b/Assets/Mirror/Core/Empty/SyncVar.cs @@ -0,0 +1 @@ +// removed 2022-11-03 diff --git a/Assets/Mirror/Runtime/SyncVar.cs.meta b/Assets/Mirror/Core/Empty/SyncVar.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncVar.cs.meta rename to Assets/Mirror/Core/Empty/SyncVar.cs.meta diff --git a/Assets/Mirror/Core/Empty/SyncVarGameObject.cs b/Assets/Mirror/Core/Empty/SyncVarGameObject.cs new file mode 100644 index 0000000..aaa3b9d --- /dev/null +++ b/Assets/Mirror/Core/Empty/SyncVarGameObject.cs @@ -0,0 +1 @@ +// removed 2022-11-03 diff --git a/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta b/Assets/Mirror/Core/Empty/SyncVarGameObject.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncVarGameObject.cs.meta rename to Assets/Mirror/Core/Empty/SyncVarGameObject.cs.meta diff --git a/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs b/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs new file mode 100644 index 0000000..aaa3b9d --- /dev/null +++ b/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs @@ -0,0 +1 @@ +// removed 2022-11-03 diff --git a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta b/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta rename to Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs.meta diff --git a/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs b/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs new file mode 100644 index 0000000..aaa3b9d --- /dev/null +++ b/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs @@ -0,0 +1 @@ +// removed 2022-11-03 diff --git a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta b/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta rename to Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs.meta diff --git a/Assets/Mirror/Core/HostMode.cs b/Assets/Mirror/Core/HostMode.cs new file mode 100644 index 0000000..e5bb2c0 --- /dev/null +++ b/Assets/Mirror/Core/HostMode.cs @@ -0,0 +1,48 @@ +// host mode related helper functions. +// usually they set up both server & client. +// it's cleaner to keep them in one place, instead of only in server / client. +using System; + +namespace Mirror +{ + public static class HostMode + { + // keep the local connections setup in one function. + // makes host setup easier to follow. + internal static void SetupConnections() + { + // create local connections pair, both are connected + Utils.CreateLocalConnections( + out LocalConnectionToClient connectionToClient, + out LocalConnectionToServer connectionToServer); + + // set client connection + NetworkClient.connection = connectionToServer; + + // set server connection + NetworkServer.SetLocalConnection(connectionToClient); + } + + // call OnConnected on server & client. + // public because NetworkClient.ConnectLocalServer was public before too. + public static void InvokeOnConnected() + { + // call server OnConnected with server's connection to client + NetworkServer.OnConnected(NetworkServer.localConnection); + + // call client OnConnected with client's connection to server + // => previously we used to send a ConnectMessage to + // NetworkServer.localConnection. this would queue the message + // until NetworkClient.Update processes it. + // => invoking the client's OnConnected event directly here makes + // tests fail. so let's do it exactly the same order as before by + // queueing the event for next Update! + //OnConnectedEvent?.Invoke(connection); + ((LocalConnectionToServer)NetworkClient.connection).QueueConnectedEvent(); + } + + // DEPRECATED 2023-01-28 + [Obsolete("ActivateHostScene did nothing, since identities all had .isClient set in NetworkServer.SpawnObjects.")] + public static void ActivateHostScene() {} + } +} diff --git a/Assets/Mirror/Core/HostMode.cs.meta b/Assets/Mirror/Core/HostMode.cs.meta new file mode 100644 index 0000000..bf6faed --- /dev/null +++ b/Assets/Mirror/Core/HostMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d27175a08d5341fc97645b49ee533d5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/InterestManagement.cs b/Assets/Mirror/Core/InterestManagement.cs similarity index 97% rename from Assets/Mirror/Runtime/InterestManagement.cs rename to Assets/Mirror/Core/InterestManagement.cs index ab149c3..8556831 100644 --- a/Assets/Mirror/Runtime/InterestManagement.cs +++ b/Assets/Mirror/Core/InterestManagement.cs @@ -12,7 +12,8 @@ public abstract class InterestManagement : MonoBehaviour // Awake configures InterestManagement in NetworkServer/Client // Do NOT check for active server or client here. // Awake must always set the static aoi references. - void Awake() + // make sure to call base.Awake when overwriting! + protected virtual void Awake() { if (NetworkServer.aoi == null) { diff --git a/Assets/Mirror/Runtime/InterestManagement.cs.meta b/Assets/Mirror/Core/InterestManagement.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/InterestManagement.cs.meta rename to Assets/Mirror/Core/InterestManagement.cs.meta diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs b/Assets/Mirror/Core/LocalConnectionToClient.cs similarity index 100% rename from Assets/Mirror/Runtime/LocalConnectionToClient.cs rename to Assets/Mirror/Core/LocalConnectionToClient.cs diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta b/Assets/Mirror/Core/LocalConnectionToClient.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta rename to Assets/Mirror/Core/LocalConnectionToClient.cs.meta diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs b/Assets/Mirror/Core/LocalConnectionToServer.cs similarity index 100% rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs rename to Assets/Mirror/Core/LocalConnectionToServer.cs diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta b/Assets/Mirror/Core/LocalConnectionToServer.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta rename to Assets/Mirror/Core/LocalConnectionToServer.cs.meta diff --git a/Assets/Mirror/Runtime/Messages.cs b/Assets/Mirror/Core/Messages.cs similarity index 78% rename from Assets/Mirror/Runtime/Messages.cs rename to Assets/Mirror/Core/Messages.cs index d3816f8..6ba0bf0 100644 --- a/Assets/Mirror/Runtime/Messages.cs +++ b/Assets/Mirror/Core/Messages.cs @@ -3,6 +3,14 @@ namespace Mirror { + // need to send time every sendInterval. + // batching automatically includes remoteTimestamp. + // all we need to do is ensure that an empty message is sent. + // and react to it. + // => we don't want to insert a snapshot on every batch. + // => do it exactly every sendInterval on every TimeSnapshotMessage. + public struct TimeSnapshotMessage : NetworkMessage {} + public struct ReadyMessage : NetworkMessage {} public struct NotReadyMessage : NetworkMessage {} @@ -28,7 +36,7 @@ public struct CommandMessage : NetworkMessage { public uint netId; public byte componentIndex; - public int functionHash; + public ushort functionHash; // the parameters for the Cmd function // -> ArraySegment to avoid unnecessary allocations public ArraySegment payload; @@ -38,12 +46,21 @@ public struct RpcMessage : NetworkMessage { public uint netId; public byte componentIndex; - public int functionHash; + public ushort functionHash; // the parameters for the Cmd function // -> ArraySegment to avoid unnecessary allocations public ArraySegment payload; } + // holds multiple buffered rpcs for the given connection. + // more efficient than sending one message per rpc. + public struct RpcBufferMessage : NetworkMessage + { + // payload contains multiple serialized RpcMessages. + // but without the message header. + public ArraySegment payload; + } + public struct SpawnMessage : NetworkMessage { // netId of new or existing object @@ -53,7 +70,7 @@ public struct SpawnMessage : NetworkMessage public bool isOwner; public ulong sceneId; // If sceneId != 0 then it is used instead of assetId - public Guid assetId; + public uint assetId; // Local position public Vector3 position; // Local rotation diff --git a/Assets/Mirror/Runtime/Messages.cs.meta b/Assets/Mirror/Core/Messages.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Messages.cs.meta rename to Assets/Mirror/Core/Messages.cs.meta diff --git a/Assets/Mirror/Core/Mirror.asmdef b/Assets/Mirror/Core/Mirror.asmdef new file mode 100644 index 0000000..59e32aa --- /dev/null +++ b/Assets/Mirror/Core/Mirror.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Mirror", + "rootNamespace": "", + "references": [ + "GUID:325984b52e4128546bc7558552f8b1d2", + "GUID:725ee7191c021de4dbf9269590ded755", + "GUID:6806a62c384838046a3c66c44f06d75f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Mirror.asmdef.meta b/Assets/Mirror/Core/Mirror.asmdef.meta similarity index 100% rename from Assets/Mirror/Runtime/Mirror.asmdef.meta rename to Assets/Mirror/Core/Mirror.asmdef.meta diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs b/Assets/Mirror/Core/NetworkAuthenticator.cs similarity index 93% rename from Assets/Mirror/Runtime/NetworkAuthenticator.cs rename to Assets/Mirror/Core/NetworkAuthenticator.cs index 9f99b50..aa1e7f7 100644 --- a/Assets/Mirror/Runtime/NetworkAuthenticator.cs +++ b/Assets/Mirror/Core/NetworkAuthenticator.cs @@ -25,7 +25,7 @@ public virtual void OnStartServer() {} /// Called when server stops, used to unregister message handlers if needed. public virtual void OnStopServer() {} - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate public virtual void OnServerAuthenticate(NetworkConnectionToClient conn) {} protected void ServerAccept(NetworkConnectionToClient conn) @@ -44,7 +44,7 @@ public virtual void OnStartClient() {} /// Called when client stops, used to unregister message handlers if needed. public virtual void OnStopClient() {} - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate public virtual void OnClientAuthenticate() {} protected void ClientAccept() diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta b/Assets/Mirror/Core/NetworkAuthenticator.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta rename to Assets/Mirror/Core/NetworkAuthenticator.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs similarity index 68% rename from Assets/Mirror/Runtime/NetworkBehaviour.cs rename to Assets/Mirror/Core/NetworkBehaviour.cs index 94cd930..630d3af 100644 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -6,14 +6,28 @@ namespace Mirror { + // SyncMode decides if a component is synced to all observers, or only owner public enum SyncMode { Observers, Owner } + // SyncDirection decides if a component is synced from: + // * server to all clients + // * owner client, to server, to all other clients + // + // naming: 'ClientToServer' etc. instead of 'ClientAuthority', because + // that wouldn't be accurate. server's OnDeserialize can still validate + // client data before applying. it's really about direction, not authority. + public enum SyncDirection { ServerToClient, ClientToServer } + /// Base class for networked components. [AddComponentMenu("")] [RequireComponent(typeof(NetworkIdentity))] [HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")] public abstract class NetworkBehaviour : MonoBehaviour { + /// Sync direction for OnSerialize. ServerToClient by default. ClientToServer for client authority. + [Tooltip("Server Authority calls OnSerialize on the server and syncs it to clients.\n\nClient Authority calls OnSerialize on the owning client, syncs it to server, which then broadcasts it to all other clients.\n\nUse server authority for cheat safety.")] + [HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient; + /// sync mode for OnSerialize // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. [Tooltip("By default synced data is sent from the server to all Observers of the object.\nChange this to Owner to only have the server update the client that has ownership authority for this object")] @@ -44,8 +58,31 @@ public abstract class NetworkBehaviour : MonoBehaviour /// True if this object is on the client-only, not host. public bool isClientOnly => netIdentity.isClientOnly; - /// True on client if that component has been assigned to the client. E.g. player, pets, henchmen. - public bool hasAuthority => netIdentity.hasAuthority; + /// isOwned is true on the client if this NetworkIdentity is one of the .owned entities of our connection on the server. + // for example: main player & pets are owned. monsters & npcs aren't. + public bool isOwned => netIdentity.isOwned; + + // Deprecated 2022-10-13 + [Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")] + public bool hasAuthority => isOwned; + + /// authority is true if we are allowed to modify this component's state. On server, it's true if SyncDirection is ServerToClient. On client, it's true if SyncDirection is ClientToServer and(!) if this object is owned by the client. + // on the client: if owned and if clientAuthority sync direction + // on the server: if serverAuthority sync direction + // + // for example, NetworkTransform: + // client may modify position if ClientAuthority mode and owned + // server may modify position only if server authority + // + // note that in original Mirror, hasAuthority only meant 'isOwned'. + // there was no syncDirection to check. + // + // also note that this is a per-NetworkBehaviour flag. + // another component may not be client authoritative, etc. + public bool authority => + isClient + ? syncDirection == SyncDirection.ClientToServer && isOwned + : syncDirection == SyncDirection.ServerToClient; /// The unique network Id of this object (unique at runtime). public uint netId => netIdentity.netId; @@ -71,7 +108,7 @@ public abstract class NetworkBehaviour : MonoBehaviour public NetworkIdentity netIdentity { get; internal set; } /// Returns the index of the component on this object - public int ComponentIndex { get; internal set; } + public byte ComponentIndex { get; internal set; } // to avoid fully serializing entities every time, we have two options: // * run a delta compression algorithm @@ -102,10 +139,6 @@ public abstract class NetworkBehaviour : MonoBehaviour protected bool GetSyncVarHookGuard(ulong dirtyBit) => (syncVarHookGuard & dirtyBit) != 0UL; - // Deprecated 2021-09-16 (old weavers used it) - [Obsolete("Renamed to GetSyncVarHookGuard (uppercase)")] - protected bool getSyncVarHookGuard(ulong dirtyBit) => GetSyncVarHookGuard(dirtyBit); - // USED BY WEAVER to set syncvars in host mode without deadlocking protected void SetSyncVarHookGuard(ulong dirtyBit, bool value) { @@ -117,31 +150,40 @@ protected void SetSyncVarHookGuard(ulong dirtyBit, bool value) syncVarHookGuard &= ~dirtyBit; } - // Deprecated 2021-09-16 (old weavers used it) - [Obsolete("Renamed to SetSyncVarHookGuard (uppercase)")] - protected void setSyncVarHookGuard(ulong dirtyBit, bool value) => SetSyncVarHookGuard(dirtyBit, value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void SetSyncObjectDirtyBit(ulong dirtyBit) + { + syncObjectDirtyBits |= dirtyBit; + } /// Set as dirty so that it's synced to clients again. // these are masks, not bit numbers, ie. 110011b not '2' for 2nd bit. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetSyncVarDirtyBit(ulong dirtyBit) { syncVarDirtyBits |= dirtyBit; } - // Deprecated 2021-09-19 - [Obsolete("SetDirtyBit was renamed to SetSyncVarDirtyBit because that's what it does")] - public void SetDirtyBit(ulong dirtyBit) => SetSyncVarDirtyBit(dirtyBit); + /// Set as dirty to trigger OnSerialize & send. Dirty bits are cleared after the send. + // previously one had to use SetSyncVarDirtyBit(1), which is confusing. + // simply reuse SetSyncVarDirtyBit for now. + // instead of adding another field. + // syncVarDirtyBits does trigger OnSerialize as well. + // + // it's important to set _all_ bits as dirty. + // for example, server needs to broadcast ClientToServer components. + // if we only set the first bit, only that SyncVar would be broadcast. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetDirty() => SetSyncVarDirtyBit(ulong.MaxValue); // true if syncInterval elapsed and any SyncVar or SyncObject is dirty - public bool IsDirty() - { - if (NetworkTime.localTime - lastSyncTime >= syncInterval) - { - // OR both bitmasks. != 0 if either was dirty. - return (syncVarDirtyBits | syncObjectDirtyBits) != 0UL; - } - return false; - } + // OR both bitmasks. != 0 if either was dirty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty() => + // check bits first. this is basically free. + (syncVarDirtyBits | syncObjectDirtyBits) != 0UL && + // only check time if bits were dirty. this is more expensive. + NetworkTime.localTime - lastSyncTime >= syncInterval; /// Clears all the dirty bits that were set by SetDirtyBits() // automatically invoked when an update is sent for this object, but can @@ -177,14 +219,73 @@ protected void InitSyncObject(SyncObject syncObject) // OnDirty needs to set nth bit in our dirty mask ulong nthBit = 1UL << index; - syncObject.OnDirty = () => syncObjectDirtyBits |= nthBit; - - // only record changes while we have observers. - // prevents ever growing .changes lists: - // if a monster has no observers but we keep modifing a SyncObject, - // then the changes would never be flushed and keep growing, - // because OnSerialize isn't called without observers. - syncObject.IsRecording = () => netIdentity.observers?.Count > 0; + syncObject.OnDirty = () => SetSyncObjectDirtyBit(nthBit); + + // who is allowed to modify SyncList/SyncSet/etc.: + // on client: only if owned ClientToserver + // on server: only if ServerToClient. + // but also for initial state when spawning. + // need to set a lambda because 'isClient' isn't available in + // InitSyncObject yet, which is called from the constructor. + syncObject.IsWritable = () => + { + // carefully check each mode separately to ensure correct results. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3342 + + // normally we would check isServer / isClient here. + // users may add to SyncLists before the object was spawned. + // isServer / isClient would still be false. + // so we need to check NetworkServer/Client.active here instead. + + // host mode: any ServerToClient and any local client owned + if (NetworkServer.active && NetworkClient.active) + return syncDirection == SyncDirection.ServerToClient || isOwned; + + // server only: any ServerToClient + if (NetworkServer.active) + return syncDirection == SyncDirection.ServerToClient; + + // client only: only ClientToServer and owned + if (NetworkClient.active) + { + // spawned: only ClientToServer and owned + if (netId != 0) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // not spawned (character selection previews, etc.): always allow + // fixes https://github.com/MirrorNetworking/Mirror/issues/3343 + return true; + } + + // undefined behaviour should throw to make it very obvious + throw new Exception("InitSyncObject: IsWritable: neither NetworkServer nor NetworkClient are active."); + }; + + // when do we record changes: + // on client: only if owned ClientToServer + // on server: only if we have observers. + // prevents ever growing .changes lists: + // if a monster has no observers but we keep modifing a SyncObject, + // then the changes would never be flushed and keep growing, + // because OnSerialize isn't called without observers. + syncObject.IsRecording = () => + { + // carefully check each mode separately to ensure correct results. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3342 + + // host mode: only if observed + if (isServer && isClient) return netIdentity.observers.Count > 0; + + // server only: only if observed + if (isServer) return netIdentity.observers.Count > 0; + + // client only: only ClientToServer and owned + if (isClient) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // users may add to SyncLists before the object was spawned. + // isServer / isClient would still be false. + // in that case, allow modifying but don't record changes yet. + return false; + }; } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions @@ -195,7 +296,7 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // to avoid Wrapper functions. a lot of people requested this. if (!NetworkClient.active) { - Debug.LogError($"Command Function {functionFullName} called without an active client."); + Debug.LogError($"Command Function {functionFullName} called on {name} without an active client.", gameObject); return; } @@ -207,14 +308,14 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // or client may have been set NotReady intentionally, so // only warn if on the reliable channel. if (channelId == Channels.Reliable) - Debug.LogWarning("Send command attempted while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady."); + Debug.LogWarning($"Command Function {functionFullName} called on {name} while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady.", gameObject); return; } // local players can always send commands, regardless of authority, other objects must have authority. - if (!(!requiresAuthority || isLocalPlayer || hasAuthority)) + if (!(!requiresAuthority || isLocalPlayer || isOwned)) { - Debug.LogWarning($"Trying to send command for object without authority. {functionFullName}"); + Debug.LogWarning($"Command Function {functionFullName} called on {name} without authority.", gameObject); return; } @@ -225,7 +326,7 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // => see also: https://github.com/vis2k/Mirror/issues/2629 if (NetworkClient.connection == null) { - Debug.LogError("Send command attempted with no client running."); + Debug.LogError($"Command Function {functionFullName} called on {name} with no client running.", gameObject); return; } @@ -233,9 +334,9 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer CommandMessage message = new CommandMessage { netId = netId, - componentIndex = (byte)ComponentIndex, + componentIndex = ComponentIndex, // type+func so Inventory.RpcUse != Equipment.RpcUse - functionHash = functionFullName.GetStableHashCode(), + functionHash = (ushort)functionFullName.GetStableHashCode(), // segment to avoid reader allocations payload = writer.ToArraySegment() }; @@ -245,6 +346,8 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // false. other objects don't have a .connectionToServer. // => so we always need to use NetworkClient.connection instead. // => see also: https://github.com/vis2k/Mirror/issues/2629 + // This bypasses the null check in NetworkClient.Send but we have + // a null check above with a detailed error log. NetworkClient.connection.Send(message, channelId); } @@ -254,14 +357,14 @@ protected void SendRPCInternal(string functionFullName, NetworkWriter writer, in // this was in Weaver before if (!NetworkServer.active) { - Debug.LogError($"RPC Function {functionFullName} called on Client."); + Debug.LogError($"RPC Function {functionFullName} called on Client.", gameObject); return; } // This cannot use NetworkServer.active, as that is not specific to this object. if (!isServer) { - Debug.LogWarning($"ClientRpc {functionFullName} called on un-spawned object: {name}"); + Debug.LogWarning($"ClientRpc {functionFullName} called on un-spawned object: {name}", gameObject); return; } @@ -269,14 +372,36 @@ protected void SendRPCInternal(string functionFullName, NetworkWriter writer, in RpcMessage message = new RpcMessage { netId = netId, - componentIndex = (byte)ComponentIndex, + componentIndex = ComponentIndex, // type+func so Inventory.RpcUse != Equipment.RpcUse - functionHash = functionFullName.GetStableHashCode(), + functionHash = (ushort)functionFullName.GetStableHashCode(), // segment to avoid reader allocations payload = writer.ToArraySegment() }; - NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId); + // serialize it to each ready observer's connection's rpc buffer. + // send them all at once, instead of sending one message per rpc. + // NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId); + + // safety check used to be in SendToReadyObservers. keep it for now. + if (netIdentity.observers != null && netIdentity.observers.Count > 0) + { + // serialize the message only once + using (NetworkWriterPooled serialized = NetworkWriterPool.Get()) + { + serialized.Write(message); + + // add to every observer's connection's rpc buffer + foreach (NetworkConnectionToClient conn in netIdentity.observers.Values) + { + bool isOwner = conn == netIdentity.connectionToClient; + if ((!isOwner || includeOwner) && conn.isReady) + { + conn.BufferRpc(message, channelId); + } + } + } + } } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions @@ -284,13 +409,13 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull { if (!NetworkServer.active) { - Debug.LogError($"TargetRPC {functionFullName} called when server not active"); + Debug.LogError($"TargetRPC {functionFullName} was called on {name} when server not active.", gameObject); return; } if (!isServer) { - Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned"); + Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned.", gameObject); return; } @@ -303,13 +428,14 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull // if still null if (conn is null) { - Debug.LogError($"TargetRPC {functionFullName} was given a null connection, make sure the object has an owner or you pass in the target connection"); + Debug.LogError($"TargetRPC {functionFullName} can't be sent because it was given a null connection. Make sure {name} is owned by a connection, or if you pass a connection manually then make sure it's not null. For example, TargetRpcs can be called on Player/Pet which are owned by a connection. However, they can not be called on Monsters/Npcs which don't have an owner connection.", gameObject); return; } - if (!(conn is NetworkConnectionToClient)) + // TODO change conn type to NetworkConnectionToClient to begin with. + if (!(conn is NetworkConnectionToClient connToClient)) { - Debug.LogError($"TargetRPC {functionFullName} requires a NetworkConnectionToClient but was given {conn.GetType().Name}"); + Debug.LogError($"TargetRPC {functionFullName} called on {name} requires a NetworkConnectionToClient but was given {conn.GetType().Name}", gameObject); return; } @@ -317,14 +443,17 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull RpcMessage message = new RpcMessage { netId = netId, - componentIndex = (byte)ComponentIndex, + componentIndex = ComponentIndex, // type+func so Inventory.RpcUse != Equipment.RpcUse - functionHash = functionFullName.GetStableHashCode(), + functionHash = (ushort)functionFullName.GetStableHashCode(), // segment to avoid reader allocations payload = writer.ToArraySegment() }; - conn.Send(message, channelId); + // serialize it to the connection's rpc buffer. + // send them all at once, instead of sending one message per rpc. + // conn.Send(message, channelId); + connToClient.BufferRpc(message, channelId); } // move the [SyncVar] generated property's .set into C# to avoid much IL @@ -344,7 +473,7 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull // { // int oldValue = health; // SetSyncVar(value, ref health, 1uL); - // if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) + // if (NetworkServer.activeHost && !GetSyncVarHookGuard(1uL)) // { // SetSyncVarHookGuard(1uL, value: true); // OnChanged(oldValue, value); @@ -368,7 +497,7 @@ public void GeneratedSyncVarSetter(T value, ref T field, ulong dirtyBit, Acti // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -395,7 +524,7 @@ public void GeneratedSyncVarSetter_GameObject(GameObject value, ref GameObject f // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -422,7 +551,7 @@ public void GeneratedSyncVarSetter_NetworkIdentity(NetworkIdentity value, ref Ne // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -450,7 +579,7 @@ public void GeneratedSyncVarSetter_NetworkBehaviour(T value, ref T field, ulo // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -469,8 +598,7 @@ public static bool SyncVarGameObjectEqual(GameObject newGameObject, uint netIdFi uint newNetId = 0; if (newGameObject != null) { - NetworkIdentity identity = newGameObject.GetComponent(); - if (identity != null) + if (newGameObject.TryGetComponent(out NetworkIdentity identity)) { newNetId = identity.netId; if (newNetId == 0) @@ -493,8 +621,7 @@ protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gam uint newNetId = 0; if (newGameObject != null) { - NetworkIdentity identity = newGameObject.GetComponent(); - if (identity != null) + if (newGameObject.TryGetComponent(out NetworkIdentity identity)) { newNetId = identity.netId; if (newNetId == 0) @@ -838,7 +965,7 @@ protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdent protected static bool SyncVarNetworkBehaviourEqual(T newBehaviour, NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour { uint newNetId = 0; - int newComponentIndex = 0; + byte newComponentIndex = 0; if (newBehaviour != null) { newNetId = newBehaviour.netId; @@ -861,7 +988,7 @@ protected void SetSyncVarNetworkBehaviour(T newBehaviour, ref T behaviourFiel return; uint newNetId = 0; - int componentIndex = 0; + byte componentIndex = 0; if (newBehaviour != null) { newNetId = newBehaviour.netId; @@ -903,35 +1030,6 @@ protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehavio return behaviourField; } - // backing field for sync NetworkBehaviour - public struct NetworkBehaviourSyncVar : IEquatable - { - public uint netId; - // limited to 255 behaviours per identity - public byte componentIndex; - - public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this() - { - this.netId = netId; - this.componentIndex = (byte)componentIndex; - } - - public bool Equals(NetworkBehaviourSyncVar other) - { - return other.netId == netId && other.componentIndex == componentIndex; - } - - public bool Equals(uint netId, int componentIndex) - { - return this.netId == netId && this.componentIndex == componentIndex; - } - - public override string ToString() - { - return $"[netId:{netId} compIndex:{componentIndex}]"; - } - } - protected static bool SyncVarEqual(T value, ref T fieldValue) { // newly initialized or changed value? @@ -955,35 +1053,46 @@ protected void SetSyncVar(T value, ref T fieldValue, ulong dirtyBit) // // initialState is true for full spawns, false for delta syncs. // note: SyncVar hooks are only called when inital=false - public virtual bool OnSerialize(NetworkWriter writer, bool initialState) + public virtual void OnSerialize(NetworkWriter writer, bool initialState) { - // if initialState: write all SyncVars. - // otherwise write dirtyBits+dirty SyncVars - bool objectWritten = initialState ? SerializeObjectsAll(writer) : SerializeObjectsDelta(writer); - bool syncVarWritten = SerializeSyncVars(writer, initialState); - return objectWritten || syncVarWritten; + SerializeSyncObjects(writer, initialState); + SerializeSyncVars(writer, initialState); } /// Override to do custom deserialization (instead of SyncVars/SyncLists). Use OnSerialize too. public virtual void OnDeserialize(NetworkReader reader, bool initialState) + { + DeserializeSyncObjects(reader, initialState); + DeserializeSyncVars(reader, initialState); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void SerializeSyncObjects(NetworkWriter writer, bool initialState) + { + // if initialState: write all SyncVars. + // otherwise write dirtyBits+dirty SyncVars + if (initialState) + SerializeObjectsAll(writer); + else + SerializeObjectsDelta(writer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void DeserializeSyncObjects(NetworkReader reader, bool initialState) { if (initialState) { - DeSerializeObjectsAll(reader); + DeserializeObjectsAll(reader); } else { - DeSerializeObjectsDelta(reader); + DeserializeObjectsDelta(reader); } - - DeserializeSyncVars(reader, initialState); } // USED BY WEAVER - protected virtual bool SerializeSyncVars(NetworkWriter writer, bool initialState) + protected virtual void SerializeSyncVars(NetworkWriter writer, bool initialState) { - return false; - // SyncVar are written here in subclass // if initialState @@ -1005,23 +1114,20 @@ protected virtual void DeserializeSyncVars(NetworkReader reader, bool initialSta // read dirty SyncVars } - public bool SerializeObjectsAll(NetworkWriter writer) + public void SerializeObjectsAll(NetworkWriter writer) { - bool dirty = false; for (int i = 0; i < syncObjects.Count; i++) { SyncObject syncObject = syncObjects[i]; syncObject.OnSerializeAll(writer); - dirty = true; } - return dirty; } - public bool SerializeObjectsDelta(NetworkWriter writer) + public void SerializeObjectsDelta(NetworkWriter writer) { - bool dirty = false; // write the mask writer.WriteULong(syncObjectDirtyBits); + // serializable objects, such as synclists for (int i = 0; i < syncObjects.Count; i++) { @@ -1030,13 +1136,11 @@ public bool SerializeObjectsDelta(NetworkWriter writer) if ((syncObjectDirtyBits & (1UL << i)) != 0) { syncObject.OnSerializeDelta(writer); - dirty = true; } } - return dirty; } - internal void DeSerializeObjectsAll(NetworkReader reader) + internal void DeserializeObjectsAll(NetworkReader reader) { for (int i = 0; i < syncObjects.Count; i++) { @@ -1045,7 +1149,7 @@ internal void DeSerializeObjectsAll(NetworkReader reader) } } - internal void DeSerializeObjectsDelta(NetworkReader reader) + internal void DeserializeObjectsDelta(NetworkReader reader) { ulong dirty = reader.ReadULong(); for (int i = 0; i < syncObjects.Count; i++) @@ -1059,6 +1163,130 @@ internal void DeSerializeObjectsDelta(NetworkReader reader) } } + // safely serialize each component in a way that one reading too much or + // too few bytes will show obvious, easy to resolve error messages. + // + // prevents the original UNET bug which started Mirror: + // https://github.com/vis2k/Mirror/issues/2617 + // where one component would read too much, and then all following reads + // on other entities would be mismatched, causing the weirdest errors. + // + // reads <> for 100% safety. + internal void Serialize(NetworkWriter writer, bool initialState) + { + // reserve length header to ensure the correct amount will be read. + // originally we used a 4 byte header (too bandwidth heavy). + // instead, let's "& 0xFF" the size. + // + // this is cleaner than barriers at the end of payload, because: + // - ensures the correct safety is read _before_ payload. + // - it's quite hard to break the check. + // a component would need to read/write the intented amount + // multiplied by 255 in order to miss the check. + // with barriers, reading 1 byte too much may still succeed if the + // next component's first byte matches the expected barrier. + // - we can still attempt to correct the invalid position via the + // safety length byte (we know that one is correct). + // + // it's just overall cleaner, and still low on bandwidth. + + // write placeholder length byte + // (jumping back later is WAY faster than allocating a temporary + // writer for the payload, then writing payload.size, payload) + int headerPosition = writer.Position; + writer.WriteByte(0); + int contentPosition = writer.Position; + + // write payload + try + { + // note this may not write anything if no syncIntervals elapsed + OnSerialize(writer, initialState); + } + catch (Exception e) + { + // show a detailed error and let the user know what went wrong + Debug.LogError($"OnSerialize failed for: object={name} component={GetType()} sceneId={netIdentity.sceneId:X}\n\n{e}"); + } + int endPosition = writer.Position; + + // fill in length hash as the last byte of the 4 byte length + writer.Position = headerPosition; + int size = endPosition - contentPosition; + byte safety = (byte)(size & 0xFF); + writer.WriteByte(safety); + writer.Position = endPosition; + + //Debug.Log($"OnSerializeSafely written for object {name} component:{GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}"); + } + + // correct the read size with the 1 byte length hash (by mischa). + // -> the component most likely read a few too many/few bytes. + // -> we know the correct last byte of the expected size (=the safety). + // -> attempt to reconstruct the size via safety byte. + // it will be correct unless someone wrote way way too much, + // as in > 255 bytes worth too much. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int ErrorCorrection(int size, byte safety) + { + // clear the last byte which most likely contains the error + uint cleared = (uint)size & 0xFFFFFF00; + + // insert the safety which we know to be correct + return (int)(cleared | safety); + } + + // returns false in case of errors. + // server needs to know in order to disconnect on error. + internal bool Deserialize(NetworkReader reader, bool initialState) + { + // detect errors, but attempt to correct before returning + bool result = true; + + // read 1 byte length hash safety & capture beginning for size check + byte safety = reader.ReadByte(); + int chunkStart = reader.Position; + + // call OnDeserialize and wrap it in a try-catch block so there's no + // way to mess up another component's deserialization + try + { + //Debug.Log($"OnDeserializeSafely: {name} component:{GetType()} sceneId:{sceneId:X} length:{contentSize}"); + OnDeserialize(reader, initialState); + } + catch (Exception e) + { + // show a detailed error and let the user know what went wrong + Debug.LogError($"OnDeserialize failed Exception={e.GetType()} (see below) object={name} component={GetType()} netId={netId}. Possible Reasons:\n" + + $" * Do {GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data? \n" + + $" * Was there an exception in {GetType()}'s OnSerialize/OnDeserialize code?\n" + + $" * Are the server and client the exact same project?\n" + + $" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" + + $"Exception {e}"); + result = false; + } + + // compare bytes read with length hash + int size = reader.Position - chunkStart; + byte sizeHash = (byte)(size & 0xFF); + if (sizeHash != safety) + { + // warn the user. + Debug.LogWarning($"{name} (netId={netId}): {GetType()} OnDeserialize size mismatch. It read {size} bytes, which caused a size hash mismatch of {sizeHash:X2} vs. {safety:X2}. Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); + + // attempt to fix the position, so the following components + // don't all fail. this is very likely to work, unless the user + // read more than 255 bytes too many / too few. + // + // see test: SerializationSizeMismatch. + int correctedSize = ErrorCorrection(size, safety); + reader.Position = chunkStart + correctedSize; + result = false; + } + + return result; + } + internal void ResetSyncObjects() { foreach (SyncObject syncObject in syncObjects) diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta b/Assets/Mirror/Core/NetworkBehaviour.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkBehaviour.cs.meta rename to Assets/Mirror/Core/NetworkBehaviour.cs.meta diff --git a/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs new file mode 100644 index 0000000..e9ed726 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs @@ -0,0 +1,33 @@ +using System; + +namespace Mirror +{ + // backing field for sync NetworkBehaviour + public struct NetworkBehaviourSyncVar : IEquatable + { + public uint netId; + // limited to 255 behaviours per identity + public byte componentIndex; + + public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this() + { + this.netId = netId; + this.componentIndex = (byte)componentIndex; + } + + public bool Equals(NetworkBehaviourSyncVar other) + { + return other.netId == netId && other.componentIndex == componentIndex; + } + + public bool Equals(uint netId, int componentIndex) + { + return this.netId == netId && this.componentIndex == componentIndex; + } + + public override string ToString() + { + return $"[netId:{netId} compIndex:{componentIndex}]"; + } + } +} diff --git a/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta new file mode 100644 index 0000000..47e3893 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b04fe7518657486089dfaf811db0b3ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs similarity index 74% rename from Assets/Mirror/Runtime/NetworkClient.cs rename to Assets/Mirror/Core/NetworkClient.cs index e5dabe3..c94eea1 100644 --- a/Assets/Mirror/Runtime/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -18,8 +18,21 @@ public enum ConnectState } /// NetworkClient with connection to server. - public static class NetworkClient + public static partial class NetworkClient { + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + // + // additionally, server & client need to use the same send interval. + // otherwise it's too easy to accidentally cause interpolation issues if + // a component sends with client.interval but interpolates with + // server.interval, etc. + public static int sendRate => NetworkServer.sendRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; + // message handlers by messageId internal static readonly Dictionary handlers = new Dictionary(); @@ -50,19 +63,24 @@ public static class NetworkClient // empty if the client has not connected yet. public static string serverIp => connection.address; - /// active is true while a client is connecting/connected + /// active is true while a client is connecting/connected either as standalone or as host client. // (= while the network is active) public static bool active => connectState == ConnectState.Connecting || connectState == ConnectState.Connected; + /// active is true while the client is connected in host mode. + // naming consistent with NetworkServer.activeHost. + public static bool activeHost => connection is LocalConnectionToServer; + /// Check if client is connecting (before connected). public static bool isConnecting => connectState == ConnectState.Connecting; /// Check if client is connected (after connecting). public static bool isConnected => connectState == ConnectState.Connected; - /// True if client is running in host mode. - public static bool isHostClient => connection is LocalConnectionToServer; + // Deprecated 2022-12-12 + [Obsolete("NetworkClient.isHostClient was renamed to .activeHost to be more obvious")] + public static bool isHostClient => activeHost; // OnConnected / OnDisconnected used to be NetworkMessages that were // invoked. this introduced a bug where external clients could send @@ -71,19 +89,19 @@ public static class NetworkClient // => public so that custom NetworkManagers can hook into it public static Action OnConnectedEvent; public static Action OnDisconnectedEvent; - public static Action OnErrorEvent; + public static Action OnErrorEvent; /// Registered spawnable prefabs by assetId. - public static readonly Dictionary prefabs = - new Dictionary(); + public static readonly Dictionary prefabs = + new Dictionary(); - // custom spawn / unspawn handlers. + // custom spawn / unspawn handlers by assetId. // useful to support prefab pooling etc.: // https://mirror-networking.gitbook.io/docs/guides/gameobjects/custom-spawnfunctions - internal static readonly Dictionary spawnHandlers = - new Dictionary(); - internal static readonly Dictionary unspawnHandlers = - new Dictionary(); + internal static readonly Dictionary spawnHandlers = + new Dictionary(); + internal static readonly Dictionary unspawnHandlers = + new Dictionary(); // spawning // internal for tests @@ -105,86 +123,62 @@ public static class NetworkClient // initialization ////////////////////////////////////////////////////// static void AddTransportHandlers() { + // community Transports may forget to call OnDisconnected. + // which could cause handlers to be added twice with +=. + // ensure we always clear the old ones first. + // fixes: https://github.com/vis2k/Mirror/issues/3152 + RemoveTransportHandlers(); + // += so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnClientConnected += OnTransportConnected; - Transport.activeTransport.OnClientDataReceived += OnTransportData; - Transport.activeTransport.OnClientDisconnected += OnTransportDisconnected; - Transport.activeTransport.OnClientError += OnError; + Transport.active.OnClientConnected += OnTransportConnected; + Transport.active.OnClientDataReceived += OnTransportData; + Transport.active.OnClientDisconnected += OnTransportDisconnected; + Transport.active.OnClientError += OnTransportError; } static void RemoveTransportHandlers() { // -= so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnClientConnected -= OnTransportConnected; - Transport.activeTransport.OnClientDataReceived -= OnTransportData; - Transport.activeTransport.OnClientDisconnected -= OnTransportDisconnected; - Transport.activeTransport.OnClientError -= OnError; + Transport.active.OnClientConnected -= OnTransportConnected; + Transport.active.OnClientDataReceived -= OnTransportData; + Transport.active.OnClientDisconnected -= OnTransportDisconnected; + Transport.active.OnClientError -= OnTransportError; } - internal static void RegisterSystemHandlers(bool hostMode) + // connect ///////////////////////////////////////////////////////////// + // initialize is called before every connect + static void Initialize(bool hostMode) { - // host mode client / remote client react to some messages differently. - // but we still need to add handlers for all of them to avoid - // 'message id not found' errors. - if (hostMode) - { - RegisterHandler(OnHostClientObjectDestroy); - RegisterHandler(OnHostClientObjectHide); - RegisterHandler(_ => {}, false); - RegisterHandler(OnHostClientSpawn); - // host mode doesn't need spawning - RegisterHandler(_ => {}); - // host mode doesn't need spawning - RegisterHandler(_ => {}); - // host mode doesn't need state updates - RegisterHandler(_ => {}); - } - else - { - RegisterHandler(OnObjectDestroy); - RegisterHandler(OnObjectHide); - RegisterHandler(NetworkTime.OnClientPong, false); - RegisterHandler(OnSpawn); - RegisterHandler(OnObjectSpawnStarted); - RegisterHandler(OnObjectSpawnFinished); - RegisterHandler(OnEntityStateMessage); - } + // Debug.Log($"Client Connect: {address}"); + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.active' first"); - // These handlers are the same for host and remote clients - RegisterHandler(OnChangeOwner); - RegisterHandler(OnRPCMessage); + // reset time interpolation on every new connect. + // ensures last sessions' state is cleared before starting again. + InitTimeInterpolation(); + + RegisterMessageHandlers(hostMode); + Transport.active.enabled = true; } - // connect ///////////////////////////////////////////////////////////// /// Connect client to a NetworkServer by address. public static void Connect(string address) { - // Debug.Log($"Client Connect: {address}"); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); + Initialize(false); - RegisterSystemHandlers(false); - Transport.activeTransport.enabled = true; AddTransportHandlers(); - connectState = ConnectState.Connecting; - Transport.activeTransport.ClientConnect(address); - + Transport.active.ClientConnect(address); connection = new NetworkConnectionToServer(); } /// Connect client to a NetworkServer by Uri. public static void Connect(Uri uri) { - // Debug.Log($"Client Connect: {uri}"); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); + Initialize(false); - RegisterSystemHandlers(false); - Transport.activeTransport.enabled = true; AddTransportHandlers(); - connectState = ConnectState.Connecting; - Transport.activeTransport.ClientConnect(uri); - + Transport.active.ClientConnect(uri); connection = new NetworkConnectionToServer(); } @@ -192,42 +186,14 @@ public static void Connect(Uri uri) // called from NetworkManager.FinishStartHost() public static void ConnectHost() { - //Debug.Log("Client Connect Host to Server"); - - RegisterSystemHandlers(true); - + Initialize(true); connectState = ConnectState.Connected; - - // create local connection objects and connect them - LocalConnectionToServer connectionToServer = new LocalConnectionToServer(); - LocalConnectionToClient connectionToClient = new LocalConnectionToClient(); - connectionToServer.connectionToClient = connectionToClient; - connectionToClient.connectionToServer = connectionToServer; - - connection = connectionToServer; - - // create server connection to local client - NetworkServer.SetLocalConnection(connectionToClient); + HostMode.SetupConnections(); } - /// Connect host mode - // called from NetworkManager.StartHostClient - // TODO why are there two connect host methods? - public static void ConnectLocalServer() - { - // call server OnConnected with server's connection to client - NetworkServer.OnConnected(NetworkServer.localConnection); - - // call client OnConnected with client's connection to server - // => previously we used to send a ConnectMessage to - // NetworkServer.localConnection. this would queue the message - // until NetworkClient.Update processes it. - // => invoking the client's OnConnected event directly here makes - // tests fail. so let's do it exactly the same order as before by - // queueing the event for next Update! - //OnConnectedEvent?.Invoke(connection); - ((LocalConnectionToServer)connection).QueueConnectedEvent(); - } + // Deprecated 2022-12-12 + [Obsolete("NetworkClient.ConnectLocalServer was moved to HostMode.InvokeOnConnected")] + public static void ConnectLocalServer() => HostMode.InvokeOnConnected(); // disconnect ////////////////////////////////////////////////////////// /// Disconnect from server. @@ -280,7 +246,7 @@ static void OnTransportConnected() // helper function static bool UnpackAndInvoke(NetworkReader reader, int channelId) { - if (MessagePacking.Unpack(reader, out ushort msgType)) + if (NetworkMessages.UnpackId(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) @@ -347,7 +313,7 @@ internal static void OnTransportData(ArraySegment data, int channelId) unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) { // enough to read at least header size? - if (reader.Remaining >= MessagePacking.HeaderSize) + if (reader.Remaining >= NetworkMessages.IdSize) { // make remoteTimeStamp available to the user connection.remoteTimeStamp = remoteTimestamp; @@ -417,10 +383,18 @@ internal static void OnTransportDisconnected() // Raise the event before changing ConnectState // because 'active' depends on this during shutdown - if (connection != null) OnDisconnectedEvent?.Invoke(); + // + // previously OnDisconnected was only invoked if connection != null. + // however, if DNS resolve fails in Transport.Connect(), + // OnDisconnected would never be called because 'connection' is only + // created after the Transport.Connect() call. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3365 + OnDisconnectedEvent?.Invoke(); connectState = ConnectState.Disconnected; ready = false; + snapshots.Clear(); + localTimeline = 0; // now that everything was handled, clear the connection. // previously this was done in Disconnect() already, but we still @@ -432,10 +406,13 @@ internal static void OnTransportDisconnected() RemoveTransportHandlers(); } - static void OnError(Exception exception) + // transport errors are forwarded to high level + static void OnTransportError(TransportError error, string reason) { - Debug.LogException(exception); - OnErrorEvent?.Invoke(exception); + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Client Transport Error: {error}: {reason}. This is fine."); + OnErrorEvent?.Invoke(error, reason); } // send //////////////////////////////////////////////////////////////// @@ -455,20 +432,56 @@ public static void Send(T message, int channelId = Channels.Reliable) } // message handlers //////////////////////////////////////////////////// + internal static void RegisterMessageHandlers(bool hostMode) + { + // host mode client / remote client react to some messages differently. + // but we still need to add handlers for all of them to avoid + // 'message id not found' errors. + if (hostMode) + { + RegisterHandler(OnHostClientObjectDestroy); + RegisterHandler(OnHostClientObjectHide); + RegisterHandler(_ => { }, false); + RegisterHandler(OnHostClientSpawn); + // host mode doesn't need spawning + RegisterHandler(_ => { }); + // host mode doesn't need spawning + RegisterHandler(_ => { }); + // host mode doesn't need state updates + RegisterHandler(_ => { }); + } + else + { + RegisterHandler(OnObjectDestroy); + RegisterHandler(OnObjectHide); + RegisterHandler(NetworkTime.OnClientPong, false); + RegisterHandler(OnSpawn); + RegisterHandler(OnObjectSpawnStarted); + RegisterHandler(OnObjectSpawnFinished); + RegisterHandler(OnEntityStateMessage); + } + + // These handlers are the same for host and remote clients + RegisterHandler(OnTimeSnapshotMessage); + RegisterHandler(OnChangeOwner); + RegisterHandler(OnRPCBufferMessage); + } + /// Register a handler for a message type T. Most should require authentication. public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } + // we use the same WrapHandler function for server and client. // so let's wrap it to ignore the NetworkConnection parameter. // it's not needed on client. it's always NetworkClient.connection. void HandlerWrapped(NetworkConnection _, T value) => handler(value); - handlers[msgType] = MessagePacking.WrapHandler((Action) HandlerWrapped, requireAuthentication); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication); } /// Replace a handler for a particular message type. Should require authentication by default. @@ -477,8 +490,8 @@ public static void RegisterHandler(Action handler, bool requireAuthenticat public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + ushort msgType = NetworkMessages.GetId(); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Replace a handler for a particular message type. Should require authentication by default. @@ -495,24 +508,25 @@ public static bool UnregisterHandler() where T : struct, NetworkMessage { // use int to minimize collisions - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); return handlers.Remove(msgType); } // spawnable prefabs /////////////////////////////////////////////////// /// Find the registered prefab for this asset id. // Useful for debuggers - public static bool GetPrefab(Guid assetId, out GameObject prefab) + public static bool GetPrefab(uint assetId, out GameObject prefab) { prefab = null; - return assetId != Guid.Empty && - prefabs.TryGetValue(assetId, out prefab) && prefab != null; + return assetId != 0 && + prefabs.TryGetValue(assetId, out prefab) && + prefab != null; } /// Validates Prefab then adds it to prefabs dictionary. static void RegisterPrefabIdentity(NetworkIdentity prefab) { - if (prefab.assetId == Guid.Empty) + if (prefab.assetId == 0) { Debug.LogError($"Can not Register '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; @@ -550,7 +564,7 @@ static void RegisterPrefabIdentity(NetworkIdentity prefab) // Note: newAssetId can not be set on GameObjects that already have an assetId // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. - public static void RegisterPrefab(GameObject prefab, Guid newAssetId) + public static void RegisterPrefab(GameObject prefab, uint newAssetId) { if (prefab == null) { @@ -558,20 +572,19 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId) return; } - if (newAssetId == Guid.Empty) + if (newAssetId == 0) { Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty"); return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); return; } - if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) + if (identity.assetId != 0 && identity.assetId != newAssetId) { Debug.LogError($"Could not register '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); return; @@ -591,8 +604,7 @@ public static void RegisterPrefab(GameObject prefab) return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); return; @@ -606,7 +618,7 @@ public static void RegisterPrefab(GameObject prefab) // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? - public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterPrefab(GameObject prefab, uint newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) @@ -628,8 +640,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); return; @@ -641,9 +652,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, return; } - Guid assetId = identity.assetId; - - if (assetId == Guid.Empty) + if (identity.assetId == 0) { Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; @@ -652,7 +661,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) { - Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + Debug.LogError($"Can not Register null SpawnHandler for {identity.assetId}"); return; } @@ -664,9 +673,9 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? - public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterPrefab(GameObject prefab, uint newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { - if (newAssetId == Guid.Empty) + if (newAssetId == 0) { Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty"); return; @@ -678,14 +687,13 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandl return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); return; } - if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) + if (identity.assetId != 0 && identity.assetId != newAssetId) { Debug.LogError($"Could not register Handler for '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); return; @@ -698,7 +706,7 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandl } identity.assetId = newAssetId; - Guid assetId = identity.assetId; + uint assetId = identity.assetId; if (spawnHandler == null) { @@ -745,8 +753,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnH return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); return; @@ -758,9 +765,9 @@ public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnH return; } - Guid assetId = identity.assetId; + uint assetId = identity.assetId; - if (assetId == Guid.Empty) + if (assetId == 0) { Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; @@ -810,14 +817,13 @@ public static void UnregisterPrefab(GameObject prefab) return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component"); return; } - Guid assetId = identity.assetId; + uint assetId = identity.assetId; prefabs.Remove(assetId); spawnHandlers.Remove(assetId); @@ -831,7 +837,7 @@ public static void UnregisterPrefab(GameObject prefab) // prefab. This should be used when no prefab exists for the spawned // objects - such as when they are constructed dynamically at runtime // from configuration data. - public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterSpawnHandler(uint assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) @@ -849,7 +855,7 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler // prefab. This should be used when no prefab exists for the spawned // objects - such as when they are constructed dynamically at runtime // from configuration data. - public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterSpawnHandler(uint assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { if (spawnHandler == null) { @@ -863,9 +869,9 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawn return; } - if (assetId == Guid.Empty) + if (assetId == 0) { - Debug.LogError("Can not Register SpawnHandler for empty Guid"); + Debug.LogError("Can not Register SpawnHandler for empty assetId"); return; } @@ -887,7 +893,7 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawn } /// Removes a registered spawn handler function that was registered with NetworkClient.RegisterHandler(). - public static void UnregisterSpawnHandler(Guid assetId) + public static void UnregisterSpawnHandler(uint assetId) { spawnHandlers.Remove(assetId); unspawnHandlers.Remove(assetId); @@ -901,7 +907,7 @@ public static void ClearSpawners() unspawnHandlers.Clear(); } - internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj) + internal static bool InvokeUnSpawnHandler(uint assetId, GameObject obj) { if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null) { @@ -964,7 +970,7 @@ internal static void InternalAddPlayer(NetworkIdentity identity) { connection.identity = identity; } - else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer"); + else Debug.LogWarning("NetworkClient can't AddPlayer before being ready. Please call NetworkClient.Ready() first. Clients are considered ready after joining the game world."); } /// Sends AddPlayer message to the server, indicating that we want to join the world. @@ -999,7 +1005,7 @@ public static bool AddPlayer() // spawning //////////////////////////////////////////////////////////// internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message) { - if (message.assetId != Guid.Empty) + if (message.assetId != 0) identity.assetId = message.assetId; if (!identity.gameObject.activeSelf) @@ -1011,23 +1017,37 @@ internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage me identity.transform.localPosition = message.position; identity.transform.localRotation = message.rotation; identity.transform.localScale = message.scale; - identity.hasAuthority = message.isOwner; + + // configure flags + // the below DeserializeClient call invokes SyncVarHooks. + // flags always need to be initialized before that. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259 + identity.isOwned = message.isOwner; identity.netId = message.netId; if (message.isLocalPlayer) InternalAddPlayer(identity); + // configure isClient/isLocalPlayer flags. + // => after InternalAddPlayer. can't initialize .isLocalPlayer + // before InternalAddPlayer sets .localPlayer + // => before DeserializeClient, otherwise SyncVar hooks wouldn't + // have isClient/isLocalPlayer set yet. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259 + InitializeIdentityFlags(identity); + // deserialize components if any payload // (Count is 0 if there were no components) if (message.payload.Count > 0) { using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload)) { - identity.OnDeserializeAllSafely(payloadReader, true); + identity.DeserializeClient(payloadReader, true); } } spawned[message.netId] = identity; + if (identity.isOwned) connection?.owned.Add(identity); // the initial spawn with OnObjectSpawnStarted/Finished calls all // object's OnStartClient/OnStartLocalPlayer after they were all @@ -1037,9 +1057,7 @@ internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage me // here immediately since there won't be another OnObjectSpawnFinished. if (isSpawnFinished) { - identity.NotifyAuthority(); - identity.OnStartClient(); - CheckForLocalPlayer(identity); + InvokeIdentityCallbacks(identity); } } @@ -1055,7 +1073,7 @@ internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity return true; } - if (message.assetId == Guid.Empty && message.sceneId == 0) + if (message.assetId == 0 && message.sceneId == 0) { Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId"); return false; @@ -1074,8 +1092,8 @@ internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity static NetworkIdentity GetExistingObject(uint netid) { - spawned.TryGetValue(netid, out NetworkIdentity localObject); - return localObject; + spawned.TryGetValue(netid, out NetworkIdentity identity); + return identity; } static NetworkIdentity SpawnPrefab(SpawnMessage message) @@ -1095,12 +1113,13 @@ static NetworkIdentity SpawnPrefab(SpawnMessage message) Debug.LogError($"Spawn Handler returned null, Handler assetId '{message.assetId}'"); return null; } - NetworkIdentity identity = obj.GetComponent(); - if (identity == null) + + if (!obj.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Object Spawned by handler did not have a NetworkIdentity, Handler assetId '{message.assetId}'"); return null; } + return identity; } @@ -1141,16 +1160,6 @@ static NetworkIdentity GetAndRemoveSceneObject(ulong sceneId) return null; } - // Checks if identity is not spawned yet, not hidden and has sceneId - static bool ConsiderForSpawning(NetworkIdentity identity) - { - // not spawned yet, not hidden, etc.? - return !identity.gameObject.activeSelf && - identity.gameObject.hideFlags != HideFlags.NotEditable && - identity.gameObject.hideFlags != HideFlags.HideAndDontSave && - identity.sceneId != 0; - } - /// Call this after loading/unloading a scene in the client after connection to register the spawnable objects public static void PrepareToSpawnSceneObjects() { @@ -1162,9 +1171,23 @@ public static void PrepareToSpawnSceneObjects() foreach (NetworkIdentity identity in allIdentities) { // add all unspawned NetworkIdentities to spawnable objects - if (ConsiderForSpawning(identity)) + // need to ensure it's not active yet because + // PrepareToSpawnSceneObjects may be called multiple times in case + // the ObjectSpawnStarted message is received multiple times. + if (Utils.IsSceneObject(identity) && + !identity.gameObject.activeSelf) { - spawnableObjects.Add(identity.sceneId, identity); + if (spawnableObjects.TryGetValue(identity.sceneId, out NetworkIdentity existingIdentity)) + { + string msg = $"NetworkClient: Duplicate sceneId {identity.sceneId} detected on {identity.gameObject.name} and {existingIdentity.gameObject.name}\n" + + $"This can happen if a networked object is persisted in DontDestroyOnLoad through loading / changing to the scene where it originated,\n" + + $"otherwise you may need to open and re-save the {identity.gameObject.scene} to reset scene id's."; + Debug.LogWarning(msg, identity.gameObject); + } + else + { + spawnableObjects.Add(identity.sceneId, identity); + } } } } @@ -1178,60 +1201,42 @@ internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _) internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _) { - //Debug.Log("SpawnFinished"); - ClearNullFromSpawned(); - // paul: Initialize the objects in the same order as they were // initialized in the server. This is important if spawned objects // use data from scene objects foreach (NetworkIdentity identity in spawned.Values.OrderBy(uv => uv.netId)) { - identity.NotifyAuthority(); - identity.OnStartClient(); - CheckForLocalPlayer(identity); - } - isSpawnFinished = true; - } - - static readonly List removeFromSpawned = new List(); - static void ClearNullFromSpawned() - { - // spawned has null objects after changing scenes on client using - // NetworkManager.ServerChangeScene remove them here so that 2nd - // loop below does not get NullReferenceException - // see https://github.com/vis2k/Mirror/pull/2240 - // TODO fix scene logic so that client scene doesn't have null objects - foreach (KeyValuePair kvp in spawned) - { - if (kvp.Value == null) + // NetworkIdentities should always be removed from .spawned when + // they are destroyed. for safety, let's double check here. + if (identity != null) { - removeFromSpawned.Add(kvp.Key); + BootstrapIdentity(identity); } + else Debug.LogWarning("Found null entry in NetworkClient.spawned. This is unexpected. Was the NetworkIdentity not destroyed properly?"); } - - // can't modify NetworkIdentity.spawned inside foreach so need 2nd loop to remove - foreach (uint id in removeFromSpawned) - { - spawned.Remove(id); - } - removeFromSpawned.Clear(); + isSpawnFinished = true; } // host mode callbacks ///////////////////////////////////////////////// static void OnHostClientObjectDestroy(ObjectDestroyMessage message) { //Debug.Log($"NetworkClient.OnLocalObjectObjDestroy netId:{message.netId}"); + + // remove from owned (if any) + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity)) + connection.owned.Remove(identity); + spawned.Remove(message.netId); } static void OnHostClientObjectHide(ObjectHideMessage message) { //Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}"); - if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && - localObject != null) + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && + identity != null) { if (aoi != null) - aoi.SetHostVisibility(localObject, false); + aoi.SetHostVisibility(identity, false); } } @@ -1239,22 +1244,21 @@ internal static void OnHostClientSpawn(SpawnMessage message) { // on host mode, the object already exist in NetworkServer.spawned. // simply add it to NetworkClient.spawned too. - if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) + if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) { - spawned[message.netId] = localObject; + spawned[message.netId] = identity; + if (message.isOwner) connection.owned.Add(identity); // now do the actual 'spawning' on host mode if (message.isLocalPlayer) - InternalAddPlayer(localObject); - - localObject.hasAuthority = message.isOwner; - localObject.NotifyAuthority(); - localObject.OnStartClient(); + InternalAddPlayer(identity); + // set visibility before invoking OnStartClient etc. callbacks if (aoi != null) - aoi.SetHostVisibility(localObject, true); + aoi.SetHostVisibility(identity, true); - CheckForLocalPlayer(localObject); + identity.isOwned = message.isOwner; + BootstrapIdentity(identity); } } @@ -1262,21 +1266,37 @@ internal static void OnHostClientSpawn(SpawnMessage message) static void OnEntityStateMessage(EntityStateMessage message) { // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); - if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) { - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload)) - localObject.OnDeserializeAllSafely(networkReader, false); + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + identity.DeserializeClient(reader, false); } else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); } static void OnRPCMessage(RpcMessage message) { - // Debug.Log($"NetworkClient.OnRPCMessage hash:{msg.functionHash} netId:{msg.netId}"); + // Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}"); if (spawned.TryGetValue(message.netId, out NetworkIdentity identity)) { - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload)) - identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, networkReader); + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, reader); + } + // Rpcs often can't be applied if interest management unspawned them + } + + static void OnRPCBufferMessage(RpcBufferMessage message) + { + // Debug.Log($"NetworkClient.OnRPCBufferMessage of {message.payload.Count} bytes"); + // parse all rpc messages from the buffer + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + while (reader.Remaining > 0) + { + // read message without header + RpcMessage rpcMessage = reader.Read(); + OnRPCMessage(rpcMessage); + } } } @@ -1315,7 +1335,15 @@ internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage me } // set ownership flag (aka authority) - identity.hasAuthority = message.isOwner; + identity.isOwned = message.isOwner; + + // Add / Remove to client's connectionToServer.owned hashset. + if (identity.isOwned) + connection?.owned.Add(identity); + else + connection?.owned.Remove(identity); + + // Call OnStartAuthority / OnStopAuthority identity.NotifyAuthority(); // set localPlayer flag @@ -1325,66 +1353,113 @@ internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage me if (identity.isLocalPlayer) { localPlayer = identity; + identity.connectionToServer = connection; + identity.OnStartLocalPlayer(); } // identity's isLocalPlayer was set to false. // clear our static localPlayer IF (and only IF) it was that one before. else if (localPlayer == identity) { localPlayer = null; + // TODO set .connectionToServer to null for old local player? + // since we set it in the above 'if' case too. } - - // call OnStartLocalPlayer if it's the local player now. - CheckForLocalPlayer(identity); } - internal static void CheckForLocalPlayer(NetworkIdentity identity) + // set up NetworkIdentity flags on the client. + // needs to be separate from invoking callbacks. + // cleaner, and some places need to set flags first. + static void InitializeIdentityFlags(NetworkIdentity identity) { - if (identity == localPlayer) - { - // Set isLocalPlayer to true on this NetworkIdentity and trigger - // OnStartLocalPlayer in all scripts on the same GO + // initialize flags before invoking callbacks. + // this way isClient/isLocalPlayer is correct during callbacks. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3362 + identity.isClient = true; + identity.isLocalPlayer = localPlayer == identity; + + // .connectionToServer is only available for local players. + // set it here, before invoking any callbacks. + // this way it's available in _all_ callbacks. + if (identity.isLocalPlayer) identity.connectionToServer = connection; + } + + // invoke NetworkIdentity callbacks on the client. + // needs to be separate from configuring flags. + // cleaner, and some places need to set flags first. + static void InvokeIdentityCallbacks(NetworkIdentity identity) + { + // invoke OnStartAuthority + identity.NotifyAuthority(); + + // invoke OnStartClient + identity.OnStartClient(); + + // invoke OnStartLocalPlayer + if (identity.isLocalPlayer) identity.OnStartLocalPlayer(); - // Debug.Log($"NetworkClient.OnOwnerMessage player:{identity.name}"); - } } - // destroy ///////////////////////////////////////////////////////////// - static void DestroyObject(uint netId) + // configure flags & invoke callbacks + static void BootstrapIdentity(NetworkIdentity identity) { - // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); - if (spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) - { - if (localObject.isLocalPlayer) - localObject.OnStopLocalPlayer(); + InitializeIdentityFlags(identity); + InvokeIdentityCallbacks(identity); + } - localObject.OnStopClient(); + // broadcast /////////////////////////////////////////////////////////// + static void BroadcastTimeSnapshot() + { + Send(new TimeSnapshotMessage(), Channels.Unreliable); + } - // custom unspawn handler for this prefab? (for prefab pools etc.) - if (InvokeUnSpawnHandler(localObject.assetId, localObject.gameObject)) - { - // reset object after user's handler - localObject.Reset(); - } - // otherwise fall back to default Destroy - else if (localObject.sceneId == 0) - { - // don't call reset before destroy so that values are still set in OnDestroy - GameObject.Destroy(localObject.gameObject); - } - // scene object.. disable it in scene instead of destroying - else + // make sure Broadcast() is only called every sendInterval. + // calling it every update() would require too much bandwidth. + static void Broadcast() + { + // joined the world yet? + if (!connection.isReady) return; + + // nothing to do in host mode. server already knows the state. + if (NetworkServer.active) return; + + // send time snapshot every sendInterval. + BroadcastTimeSnapshot(); + + // for each entity that the client owns + foreach (NetworkIdentity identity in connection.owned) + { + // make sure it's not null or destroyed. + // (which can happen if someone uses + // GameObject.Destroy instead of + // NetworkServer.Destroy) + if (identity != null) { - localObject.gameObject.SetActive(false); - spawnableObjects[localObject.sceneId] = localObject; - // reset for scene objects - localObject.Reset(); - } + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // get serialization for this entity viewed by this connection + // (if anything was serialized this time) + identity.SerializeClient(writer); + if (writer.Position > 0) + { + // send state update message + EntityStateMessage message = new EntityStateMessage + { + netId = identity.netId, + payload = writer.ToArraySegment() + }; + Send(message); - // remove from dictionary no matter how it is unspawned - spawned.Remove(netId); + // reset dirty bits so it's not resent next time. + identity.ClearDirtyComponentsDirtyBits(); + } + } + } + // spawned list should have no null entries because we + // always call Remove in OnObjectDestroy everywhere. + // if it does have null then we missed something. + else Debug.LogWarning($"Found 'null' entry in owned list for client. This is unexpected behaviour."); } - //else Debug.LogWarning($"Did not find target for destroy message for {netId}"); } // update ////////////////////////////////////////////////////////////// @@ -1393,14 +1468,49 @@ static void DestroyObject(uint netId) internal static void NetworkEarlyUpdate() { // process all incoming messages first before updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ClientEarlyUpdate(); + if (Transport.active != null) + Transport.active.ClientEarlyUpdate(); + + // time snapshot interpolation + UpdateTimeInterpolation(); } // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkLateUpdate() { + // broadcast ClientToServer components while active + // note that Broadcast() runs every update. + // on clients with 120 Hz, this will run 120 times per second. + // however, Broadcast only checks .owned, which usually aren't many. + // + // we could use a .sendInterval, but it would also put a minimum + // limit to every component's sendInterval automatically. + if (active) + { + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + // + // Unity 2019 doesn't have Time.timeAsDouble yet + if (!Application.isPlaying || + AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime)) + { + Broadcast(); + } + } + + // update connections to flush out messages _after_ broadcast // local connection? if (connection is LocalConnectionToServer localConnection) { @@ -1421,11 +1531,11 @@ internal static void NetworkLateUpdate() } // process all outgoing messages after updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ClientLateUpdate(); + if (Transport.active != null) + Transport.active.ClientLateUpdate(); } - // shutdown //////////////////////////////////////////////////////////// + // destroy ///////////////////////////////////////////////////////////// /// Destroys all networked objects on the client. // Note: NetworkServer.CleanupNetworkIdentities does the same on server. public static void DestroyAllClientObjects() @@ -1482,6 +1592,7 @@ public static void DestroyAllClientObjects() } } spawned.Clear(); + connection?.owned.Clear(); } catch (InvalidOperationException e) { @@ -1490,6 +1601,45 @@ public static void DestroyAllClientObjects() } } + static void DestroyObject(uint netId) + { + // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); + if (spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null) + { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + + identity.OnStopClient(); + + // custom unspawn handler for this prefab? (for prefab pools etc.) + if (InvokeUnSpawnHandler(identity.assetId, identity.gameObject)) + { + // reset object after user's handler + identity.Reset(); + } + // otherwise fall back to default Destroy + else if (identity.sceneId == 0) + { + // don't call reset before destroy so that values are still set in OnDestroy + GameObject.Destroy(identity.gameObject); + } + // scene object.. disable it in scene instead of destroying + else + { + identity.gameObject.SetActive(false); + spawnableObjects[identity.sceneId] = identity; + // reset for scene objects + identity.Reset(); + } + + // remove from dictionary no matter how it is unspawned + connection.owned.Remove(identity); // if any + spawned.Remove(netId); + } + //else Debug.LogWarning($"Did not find target for destroy message for {netId}"); + } + + // shutdown //////////////////////////////////////////////////////////// /// Shutdown the client. // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] @@ -1497,15 +1647,17 @@ public static void Shutdown() { //Debug.Log("Shutting down client."); + // objects need to be destroyed before spawners are cleared + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3334 + DestroyAllClientObjects(); + // calls prefabs.Clear(); // calls spawnHandlers.Clear(); // calls unspawnHandlers.Clear(); ClearSpawners(); - // calls spawned.Clear() if no exception occurs - DestroyAllClientObjects(); - spawned.Clear(); + connection?.owned.Clear(); handlers.Clear(); spawnableObjects.Clear(); @@ -1521,8 +1673,8 @@ public static void Shutdown() // we do NOT call Transport.Shutdown, because someone only called // NetworkClient.Shutdown. we can't assume that the server is // supposed to be shut down too! - if (Transport.activeTransport != null) - Transport.activeTransport.ClientDisconnect(); + if (Transport.active != null) + Transport.active.ClientDisconnect(); // reset statics connectState = ConnectState.None; @@ -1531,6 +1683,7 @@ public static void Shutdown() ready = false; isSpawnFinished = false; isLoadingScene = false; + lastSendTime = 0; unbatcher = new Unbatcher(); @@ -1540,5 +1693,30 @@ public static void Shutdown() OnDisconnectedEvent = null; OnErrorEvent = null; } + + // GUI ///////////////////////////////////////////////////////////////// + // called from NetworkManager to display timeline interpolation status. + // useful to indicate catchup / slowdown / dynamic adjustment etc. + public static void OnGUI() + { + // only if in world + if (!ready) return; + + GUILayout.BeginArea(new Rect(10, 5, 500, 50)); + + GUILayout.BeginHorizontal("Box"); + GUILayout.Label("Snapshot Interp.:"); + // color while catching up / slowing down + if (localTimescale > 1) GUI.color = Color.green; // green traffic light = go fast + else if (localTimescale < 1) GUI.color = Color.red; // red traffic light = go slow + else GUI.color = Color.white; + GUILayout.Box($"timeline: {localTimeline:F2}"); + GUILayout.Box($"buffer: {snapshots.Count}"); + GUILayout.Box($"timescale: {localTimescale:F2}"); + GUILayout.Box($"BTM: {bufferTimeMultiplier:F2}"); + GUILayout.EndHorizontal(); + + GUILayout.EndArea(); + } } } diff --git a/Assets/Mirror/Runtime/NetworkClient.cs.meta b/Assets/Mirror/Core/NetworkClient.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkClient.cs.meta rename to Assets/Mirror/Core/NetworkClient.cs.meta diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs new file mode 100644 index 0000000..060dae5 --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public static partial class NetworkClient + { + // TODO expose the settings to the user later. + // via NetMan or NetworkClientConfig or NetworkClient as component etc. + + // decrease bufferTime at runtime to see the catchup effect. + // increase to see slowdown. + // 'double' so we can have very precise dynamic adjustment without rounding + [Header("Snapshot Interpolation: Buffering")] + [Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")] + public static double bufferTimeMultiplier = 2; + public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + + // + public static SortedList snapshots = new SortedList(); + + // for smooth interpolation, we need to interpolate along server time. + // any other time (arrival on client, client local time, etc.) is not + // going to give smooth results. + // in other words, this is the remote server's time, but adjusted. + // + // internal for use from NetworkTime. + // double for long running servers, see NetworkTime comments. + internal static double localTimeline; + + // catchup / slowdown adjustments are applied to timescale, + // to be adjusted in every update instead of when receiving messages. + internal static double localTimescale = 1; + + // catchup ///////////////////////////////////////////////////////////// + // catchup thresholds in 'frames'. + // half a frame might be too aggressive. + [Header("Snapshot Interpolation: Catchup / Slowdown")] + [Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")] + public static float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots + + [Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")] + public static float catchupPositiveThreshold = 1; + + [Tooltip("Local timeline acceleration in % while catching up.")] + [Range(0, 1)] + public static double catchupSpeed = 0.01f; // 1% + + [Tooltip("Local timeline slowdown in % while slowing down.")] + [Range(0, 1)] + public static double slowdownSpeed = 0.01f; // 1% + + [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] + public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway + + // we use EMA to average the last second worth of snapshot time diffs. + // manually averaging the last second worth of values with a for loop + // would be the same, but a moving average is faster because we only + // ever add one value. + static ExponentialMovingAverage driftEma; + + // dynamic buffer time adjustment ////////////////////////////////////// + // dynamically adjusts bufferTimeMultiplier for smooth results. + // to understand how this works, try this manually: + // + // - disable dynamic adjustment + // - set jitter = 0.2 (20% is a lot!) + // - notice some stuttering + // - disable interpolation to see just how much jitter this really is(!) + // - enable interpolation again + // - manually increase bufferTimeMultiplier to 3-4 + // ... the cube slows down (blue) until it's smooth + // - with dynamic adjustment enabled, it will set 4 automatically + // ... the cube slows down (blue) until it's smooth as well + // + // note that 20% jitter is extreme. + // for this to be perfectly smooth, set the safety tolerance to '2'. + // but realistically this is not necessary, and '1' is enough. + [Header("Snapshot Interpolation: Dynamic Adjustment")] + [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] + public static bool dynamicAdjustment = true; + + [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] + public static float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments) + + [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] + public static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time + static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + + // OnValidate: see NetworkClient.cs + // add snapshot & initialize client interpolation time if needed + + // initialization called from Awake + static void InitTimeInterpolation() + { + // reset timeline, localTimescale & snapshots from last session (if any) + // Don't reset bufferTimeMultiplier here - whatever their network condition + // was when they disconnected, it won't have changed on immediate reconnect. + localTimeline = 0; + localTimescale = 1; + snapshots.Clear(); + + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * deliveryTimeEmaDuration); + } + + // server sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + +#if !UNITY_2020_3_OR_NEWER + // Unity 2019 doesn't have Time.timeAsDouble yet + OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); +#else + OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); +#endif + } + + // see comments at the top of this file + public static void OnTimeSnapshot(TimeSnapshot snap) + { + // Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}"); + + // (optional) dynamic adjustment + if (dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + dynamicAdjustmentTolerance + ); + } + + // insert into the buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snap, + ref localTimeline, + ref localTimescale, + NetworkServer.sendInterval, + bufferTime, + catchupSpeed, + slowdownSpeed, + ref driftEma, + catchupNegativeThreshold, + catchupPositiveThreshold, + ref deliveryTimeEma); + + // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}"); + } + + // call this from early update, so the timeline is safe to use in update + static void UpdateTimeInterpolation() + { + // only while we have snapshots. + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale); + + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta new file mode 100644 index 0000000..3c52ae1 --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ad039071a9cc487b9f7831d28bbe8e83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs b/Assets/Mirror/Core/NetworkConnection.cs similarity index 92% rename from Assets/Mirror/Runtime/NetworkConnection.cs rename to Assets/Mirror/Core/NetworkConnection.cs index 14729c6..6b47abb 100644 --- a/Assets/Mirror/Runtime/NetworkConnection.cs +++ b/Assets/Mirror/Core/NetworkConnection.cs @@ -10,11 +10,6 @@ public abstract class NetworkConnection { public const int LocalConnectionId = 0; - /// NetworkIdentities that this connection can see - // DEPRECATED 2022-02-05 - [Obsolete("Cast to NetworkConnectionToClient to access .observing")] - public HashSet observing => ((NetworkConnectionToClient)this).observing; - /// Unique identifier for this connection that is assigned by the transport layer. // assigned by transport, this id is unique for every connection on server. // clients don't know their own id and they don't know other client's ids. @@ -42,13 +37,12 @@ public abstract class NetworkConnection public NetworkIdentity identity { get; internal set; } /// All NetworkIdentities owned by this connection. Can be main player, pets, etc. + // .owned is now valid both on server and on client. // IMPORTANT: this needs to be , not . // fixes a bug where DestroyOwnedObjects wouldn't find the // netId anymore: https://github.com/vis2k/Mirror/issues/1380 // Works fine with NetworkIdentity pointers though. - // DEPRECATED 2022-02-05 - [Obsolete("Cast to NetworkConnectionToClient to access .clientOwnedObjects")] - public HashSet clientOwnedObjects => ((NetworkConnectionToClient)this).clientOwnedObjects; + public readonly HashSet owned = new HashSet(); // batching from server to client & client to server. // fewer transport calls give us significantly better performance/scale. @@ -91,7 +85,7 @@ protected Batcher GetBatchForChannelId(int channelId) if (!batches.TryGetValue(channelId, out batch)) { // get max batch size for this channel - int threshold = Transport.activeTransport.GetBatchThreshold(channelId); + int threshold = Transport.active.GetBatchThreshold(channelId); // create batcher batch = new Batcher(threshold); @@ -107,7 +101,7 @@ protected Batcher GetBatchForChannelId(int channelId) // => it's important to log errors, so the user knows what went wrong. protected static bool ValidatePacketSize(ArraySegment segment, int channelId) { - int max = Transport.activeTransport.GetMaxPacketSize(channelId); + int max = Transport.active.GetMaxPacketSize(channelId); if (segment.Count > max) { Debug.LogError($"NetworkConnection.ValidatePacketSize: cannot send packet larger than {max} bytes, was {segment.Count} bytes"); @@ -134,7 +128,7 @@ public void Send(T message, int channelId = Channels.Reliable) using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message and send allocation free - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1); Send(writer.ToArraySegment(), channelId); } @@ -175,15 +169,15 @@ internal virtual void Send(ArraySegment segment, int channelId = Channels. internal virtual void Update() { // go through batches for all channels + // foreach ((int key, Batcher batcher) in batches) // Unity 2020 doesn't support deconstruct yet foreach (KeyValuePair kvp in batches) { // make and send as many batches as necessary from the stored // messages. - Batcher batcher = kvp.Value; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // make a batch with our local time (double precision) - while (batcher.GetBatch(writer)) + while (kvp.Value.GetBatch(writer)) { // validate packet before handing the batch to the // transport. this guarantees that we always stay diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs.meta b/Assets/Mirror/Core/NetworkConnection.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkConnection.cs.meta rename to Assets/Mirror/Core/NetworkConnection.cs.meta diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs b/Assets/Mirror/Core/NetworkConnectionToClient.cs new file mode 100644 index 0000000..e7c0bd0 --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public class NetworkConnectionToClient : NetworkConnection + { + // rpcs are collected in a buffer, and then flushed out together. + // this way we don't need one NetworkMessage per rpc. + // => prepares for LocalWorldState as well. + // ensure max size when adding! + readonly NetworkWriter reliableRpcs = new NetworkWriter(); + readonly NetworkWriter unreliableRpcs = new NetworkWriter(); + + public override string address => + Transport.active.ServerGetClientAddress(connectionId); + + /// NetworkIdentities that this connection can see + // TODO move to server's NetworkConnectionToClient? + public readonly HashSet observing = new HashSet(); + + // Deprecated 2022-10-13 + [Obsolete(".clientOwnedObjects was renamed to .owned :)")] + public HashSet clientOwnedObjects => owned; + + // unbatcher + public Unbatcher unbatcher = new Unbatcher(); + + // server runs a time snapshot interpolation for each client's local time. + // this is necessary for client auth movement to still be smooth on the + // server for host mode. + // TODO move them along server's timeline in the future. + // perhaps with an offset. + // for now, keep compatibility by manually constructing a timeline. + ExponentialMovingAverage driftEma; + ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + public double remoteTimeline; + public double remoteTimescale; + double bufferTimeMultiplier = 2; + double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + + // + readonly SortedList snapshots = new SortedList(); + + // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients. + public int snapshotBufferSizeLimit = 64; + + public NetworkConnectionToClient(int networkConnectionId) + : base(networkConnectionId) + { + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration); + + // buffer limit should be at least multiplier to have enough in there + snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit); + } + + public void OnTimeSnapshot(TimeSnapshot snapshot) + { + // protect against ever growing buffer size attacks + if (snapshots.Count >= snapshotBufferSizeLimit) return; + + // (optional) dynamic adjustment + if (NetworkClient.dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + NetworkClient.dynamicAdjustmentTolerance + ); + // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} "); + } + + // insert into the server buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snapshot, + ref remoteTimeline, + ref remoteTimescale, + NetworkServer.sendInterval, + bufferTime, + NetworkClient.catchupSpeed, + NetworkClient.slowdownSpeed, + ref driftEma, + NetworkClient.catchupNegativeThreshold, + NetworkClient.catchupPositiveThreshold, + ref deliveryTimeEma + ); + } + + public void UpdateTimeInterpolation() + { + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale); + + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => + Transport.active.ServerSend(connectionId, segment, channelId); + + void FlushRpcs(NetworkWriter buffer, int channelId) + { + if (buffer.Position > 0) + { + Send(new RpcBufferMessage{ payload = buffer }, channelId); + buffer.Position = 0; + } + } + + // helper for both channels + void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize) + { + // calculate buffer limit. we can only fit so much into a message. + // max - message header - WriteArraySegment size header - batch header + int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize; + + // remember previous valid position + int before = buffer.Position; + + // serialize the message without header + buffer.Write(message); + + // before we potentially flush out old messages, + // let's ensure this single message can even fit the limit. + // otherwise no point in flushing. + int messageSize = buffer.Position - before; + if (messageSize > bufferLimit) + { + Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}"); + return; + } + + // too much to fit into max message size? + // then flush first, then write it again. + // (message + message header + 4 bytes WriteArraySegment header) + if (buffer.Position > bufferLimit) + { + buffer.Position = before; + FlushRpcs(buffer, channelId); // this resets position + buffer.Write(message); + } + } + + internal void BufferRpc(RpcMessage message, int channelId) + { + int maxMessageSize = Transport.active.GetMaxPacketSize(channelId); + if (channelId == Channels.Reliable) + { + BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize); + } + else if (channelId == Channels.Unreliable) + { + BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize); + } + } + + internal override void Update() + { + // send rpc buffers + FlushRpcs(reliableRpcs, Channels.Reliable); + FlushRpcs(unreliableRpcs, Channels.Unreliable); + + // call base update to flush out batched messages + base.Update(); + } + + /// Disconnects this connection. + public override void Disconnect() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + reliableRpcs.Position = 0; + unreliableRpcs.Position = 0; + Transport.active.ServerDisconnect(connectionId); + + // IMPORTANT: NetworkConnection.Disconnect() is NOT called for + // voluntary disconnects from the other end. + // -> so all 'on disconnect' cleanup code needs to be in + // OnTransportDisconnect, where it's called for both voluntary + // and involuntary disconnects! + } + + internal void AddToObserving(NetworkIdentity netIdentity) + { + observing.Add(netIdentity); + + // spawn identity for this conn + NetworkServer.ShowForConnection(netIdentity, this); + } + + internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) + { + observing.Remove(netIdentity); + + if (!isDestroyed) + { + // hide identity for this conn + NetworkServer.HideForConnection(netIdentity, this); + } + } + + internal void RemoveFromObservingsObservers() + { + foreach (NetworkIdentity netIdentity in observing) + { + netIdentity.RemoveObserver(this); + } + observing.Clear(); + } + + internal void AddOwnedObject(NetworkIdentity obj) + { + owned.Add(obj); + } + + internal void RemoveOwnedObject(NetworkIdentity obj) + { + owned.Remove(obj); + } + + internal void DestroyOwnedObjects() + { + // create a copy because the list might be modified when destroying + HashSet tmp = new HashSet(owned); + foreach (NetworkIdentity netIdentity in tmp) + { + if (netIdentity != null) + { + NetworkServer.Destroy(netIdentity.gameObject); + } + } + + // clear the hashset because we destroyed them all + owned.Clear(); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta b/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta rename to Assets/Mirror/Core/NetworkConnectionToClient.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs b/Assets/Mirror/Core/NetworkConnectionToServer.cs similarity index 86% rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs rename to Assets/Mirror/Core/NetworkConnectionToServer.cs index a1ebc5f..58e60e9 100644 --- a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs +++ b/Assets/Mirror/Core/NetworkConnectionToServer.cs @@ -10,7 +10,7 @@ public class NetworkConnectionToServer : NetworkConnection // Send stage three: hand off to transport [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => - Transport.activeTransport.ClientSend(segment, channelId); + Transport.active.ClientSend(segment, channelId); /// Disconnects this connection. public override void Disconnect() @@ -20,7 +20,7 @@ public override void Disconnect() // TODO remove redundant state. have one source of truth for .ready! isReady = false; NetworkClient.ready = false; - Transport.activeTransport.ClientDisconnect(); + Transport.active.ClientDisconnect(); } } } diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta b/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta rename to Assets/Mirror/Core/NetworkConnectionToServer.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs b/Assets/Mirror/Core/NetworkDiagnostics.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkDiagnostics.cs rename to Assets/Mirror/Core/NetworkDiagnostics.cs diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta b/Assets/Mirror/Core/NetworkDiagnostics.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta rename to Assets/Mirror/Core/NetworkDiagnostics.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs similarity index 72% rename from Assets/Mirror/Runtime/NetworkIdentity.cs rename to Assets/Mirror/Core/NetworkIdentity.cs index 6c3c122..bd3f06f 100644 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -1,17 +1,18 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Mirror.RemoteCalls; using UnityEngine; using UnityEngine.Serialization; #if UNITY_EDITOR - using UnityEditor; +using UnityEditor; - #if UNITY_2021_2_OR_NEWER - using UnityEditor.SceneManagement; - #elif UNITY_2018_3_OR_NEWER +#if UNITY_2021_2_OR_NEWER +using UnityEditor.SceneManagement; +#elif UNITY_2018_3_OR_NEWER using UnityEditor.Experimental.SceneManagement; - #endif +#endif #endif namespace Mirror @@ -88,13 +89,17 @@ public sealed class NetworkIdentity : MonoBehaviour /// True if this object exists on a client that is not also acting as a server. public bool isClientOnly => isClient && !isServer; - /// True on client if that component has been assigned to the client. E.g. player, pets, henchmen. - public bool hasAuthority { get; internal set; } + /// isOwned is true on the client if this NetworkIdentity is one of the .owned entities of our connection on the server. + // for example: main player & pets are owned. monsters & npcs aren't. + public bool isOwned { get; internal set; } + + // Deprecated 2022-10-13 + [Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")] + public bool hasAuthority => isOwned; /// The set of network connections (players) that can see this object. - // note: null until OnStartServer was called. this is necessary for - // SendTo* to work properly in server-only mode. - public Dictionary observers; + public readonly Dictionary observers = + new Dictionary(); /// The unique network Id of this object (unique at runtime). public uint netId { get; internal set; } @@ -104,6 +109,55 @@ public sealed class NetworkIdentity : MonoBehaviour [FormerlySerializedAs("m_SceneId"), HideInInspector] public ulong sceneId; + // assetId used to spawn prefabs across the network. + // originally a Guid, but a 4 byte uint is sufficient + // (as suggested by james) + // + // it's also easier to work with for serialization etc. + // serialized and visible in inspector for easier debugging + [SerializeField] uint _assetId; + + // The AssetId trick: + // Ideally we would have a serialized 'Guid m_AssetId' but Unity can't + // serialize it because Guid's internal bytes are private + // + // Using just the Guid string would work, but it's 32 chars long and + // would then be sent over the network as 64 instead of 16 bytes + // + // => The solution is to serialize the string internally here and then + // use the real 'Guid' type for everything else via .assetId + public uint assetId + { + get + { +#if UNITY_EDITOR + // old UNET comment: + // This is important because sometimes OnValidate does not run + // (like when adding NetworkIdentity to prefab with no child links) + if (_assetId == 0) + SetupIDs(); +#endif + return _assetId; + } + // assetId is set internally when creating or duplicating a prefab + internal set + { + // should never be empty + if (value == 0) + { + Debug.LogError($"Can not set AssetId to empty guid on NetworkIdentity '{name}', old assetId '{_assetId}'"); + return; + } + + // always set it otherwise. + // for new prefabs, it will set from 0 to N. + // for duplicated prefabs, it will set from N to M. + // either way, it's always set to a valid GUID. + _assetId = value; + // Debug.Log($"Setting AssetId on NetworkIdentity '{name}', new assetId '{value:X4}'"); + } + } + /// Make this object only exist when the game is running as a server (or host). [FormerlySerializedAs("m_ServerOnly")] [Tooltip("Prevents this object from being spawned / enabled on clients")] @@ -130,35 +184,14 @@ internal set } NetworkConnectionToClient _connectionToClient; - /// All spawned NetworkIdentities by netId. Available on server and client. - // server sees ALL spawned ones. - // client sees OBSERVED spawned ones. - // => split into NetworkServer.spawned and NetworkClient.spawned to - // reduce shared state between server & client. - // => prepares for NetworkServer/Client as component & better host mode. - [Obsolete("NetworkIdentity.spawned is now NetworkServer.spawned on server, NetworkClient.spawned on client.\nPrepares for NetworkServer/Client as component, better host mode, better testing.")] - public static Dictionary spawned - { - get - { - // server / host mode: use the one from server. - // host mode has access to all spawned. - if (NetworkServer.active) return NetworkServer.spawned; - - // client - if (NetworkClient.active) return NetworkClient.spawned; - - // neither: then we are testing. - // we could default to NetworkServer.spawned. - // but from the outside, that's not obvious. - // better to throw an exception to make it obvious. - throw new Exception("NetworkIdentity.spawned was accessed before NetworkServer/NetworkClient were active."); - } - } - // get all NetworkBehaviour components public NetworkBehaviour[] NetworkBehaviours { get; private set; } + // to save bandwidth, we send one 64 bit dirty mask + // instead of 1 byte index per dirty component. + // which means we can't allow > 64 components (it's enough). + const int MaxNetworkBehaviours = 64; + // current visibility // // Default = use interest management @@ -183,68 +216,43 @@ public static Dictionary spawned observersWriter = new NetworkWriter() }; - /// Prefab GUID used to spawn prefabs across the network. - // - // The AssetId trick: - // Ideally we would have a serialized 'Guid m_AssetId' but Unity can't - // serialize it because Guid's internal bytes are private - // - // UNET used 'NetworkHash128' originally, with byte0, ..., byte16 - // which works, but it just unnecessary extra code - // - // Using just the Guid string would work, but it's 32 chars long and - // would then be sent over the network as 64 instead of 16 bytes - // - // => The solution is to serialize the string internally here and then - // use the real 'Guid' type for everything else via .assetId - public Guid assetId + // Keep track of all sceneIds to detect scene duplicates + static readonly Dictionary sceneIds = + new Dictionary(); + + // Helper function to handle Command/Rpc + internal void HandleRemoteCall(byte componentIndex, ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null) { - get + // check if unity object has been destroyed + if (this == null) { -#if UNITY_EDITOR - // This is important because sometimes OnValidate does not run (like when adding view to prefab with no child links) - if (string.IsNullOrWhiteSpace(m_AssetId)) - SetupIDs(); -#endif - // convert string to Guid and use .Empty to avoid exception if - // we would use 'new Guid("")' - return string.IsNullOrWhiteSpace(m_AssetId) ? Guid.Empty : new Guid(m_AssetId); + Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]"); + return; } - internal set - { - string newAssetIdString = value == Guid.Empty ? string.Empty : value.ToString("N"); - string oldAssetIdString = m_AssetId; - // they are the same, do nothing - if (oldAssetIdString == newAssetIdString) - { - return; - } - - // new is empty - if (string.IsNullOrWhiteSpace(newAssetIdString)) - { - Debug.LogError($"Can not set AssetId to empty guid on NetworkIdentity '{name}', old assetId '{oldAssetIdString}'"); - return; - } - - // old not empty - if (!string.IsNullOrWhiteSpace(oldAssetIdString)) - { - Debug.LogError($"Can not Set AssetId on NetworkIdentity '{name}' because it already had an assetId, current assetId '{oldAssetIdString}', attempted new assetId '{newAssetIdString}'"); - return; - } + // find the right component to invoke the function on + if (componentIndex >= NetworkBehaviours.Length) + { + Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]"); + return; + } - // old is empty - m_AssetId = newAssetIdString; - // Debug.Log($"Settings AssetId on NetworkIdentity '{name}', new assetId '{newAssetIdString}'"); + NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex]; + if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection)) + { + Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}]."); } } - [SerializeField, HideInInspector] string m_AssetId; - // Keep track of all sceneIds to detect scene duplicates - static readonly Dictionary sceneIds = - new Dictionary(); + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + // internal so it can be called from NetworkServer & NetworkClient + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + internal static void ResetStatics() + { + // reset ALL statics + ResetClientStatics(); + ResetServerStatics(); + } // reset only client sided statics. // don't touch server statics when calling StopClient in host mode. @@ -260,33 +268,9 @@ internal static void ResetServerStatics() nextNetworkId = 1; } - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - // internal so it can be called from NetworkServer & NetworkClient - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - internal static void ResetStatics() - { - // reset ALL statics - ResetClientStatics(); - ResetServerStatics(); - } - /// Gets the NetworkIdentity from the sceneIds dictionary with the corresponding id public static NetworkIdentity GetSceneIdentity(ulong id) => sceneIds[id]; - // used when adding players - internal void SetClientOwner(NetworkConnectionToClient conn) - { - // do nothing if it already has an owner - if (connectionToClient != null && conn != connectionToClient) - { - Debug.LogError($"Object {this} netId={netId} already has an owner. Use RemoveClientAuthority() first", this); - return; - } - - // otherwise set the owner connection - connectionToClient = conn; - } - static uint nextNetworkId = 1; internal static uint GetNextNetworkId() => nextNetworkId++; @@ -311,15 +295,29 @@ internal void InitializeNetworkBehaviours() // Get all NetworkBehaviours // (never null. GetComponents returns [] if none found) NetworkBehaviours = GetComponents(); - if (NetworkBehaviours.Length > byte.MaxValue) - Debug.LogError($"Only {byte.MaxValue} NetworkBehaviour components are allowed for NetworkIdentity: {name} because we send the index as byte.", this); + ValidateComponents(); // initialize each one for (int i = 0; i < NetworkBehaviours.Length; ++i) { NetworkBehaviour component = NetworkBehaviours[i]; component.netIdentity = this; - component.ComponentIndex = i; + component.ComponentIndex = (byte)i; + } + } + + void ValidateComponents() + { + if (NetworkBehaviours == null) + { + Debug.LogError($"NetworkBehaviours array is null on {gameObject.name}!\n" + + $"Typically this can happen when a networked object is a child of a " + + $"non-networked parent that's disabled, preventing Awake on the networked object " + + $"from being invoked, where the NetworkBehaviours array is initialized.", gameObject); + } + else if (NetworkBehaviours.Length > MaxNetworkBehaviours) + { + Debug.LogError($"NetworkIdentity {name} has too many NetworkBehaviour components: only {MaxNetworkBehaviours} NetworkBehaviour components are allowed in order to save bandwidth.", this); } } @@ -358,7 +356,10 @@ void AssignAssetID(string path) { // only set if not empty. fixes https://github.com/vis2k/Mirror/issues/2765 if (!string.IsNullOrWhiteSpace(path)) - m_AssetId = AssetDatabase.AssetPathToGUID(path); + { + Guid guid = new Guid(AssetDatabase.AssetPathToGUID(path)); + assetId = (uint)guid.GetHashCode(); // deterministic + } } void AssignAssetID(GameObject prefab) => AssignAssetID(AssetDatabase.GetAssetPath(prefab)); @@ -557,7 +558,7 @@ void SetupIDs() // anymore because assetId was cleared if (!EditorApplication.isPlaying) { - m_AssetId = ""; + _assetId = 0; } // don't log. would show a lot when pressing play in uMMORPG/uSurvival/etc. //else Debug.Log($"Avoided clearing assetId at runtime for {name} after (probably) clicking any of the NetworkIdentity properties."); @@ -612,55 +613,25 @@ void OnDestroy() if (NetworkClient.localPlayer == this) NetworkClient.localPlayer = null; } - } - - internal void OnStartServer() - { - // do nothing if already spawned - if (isServer) - return; - - // set isServer flag - isServer = true; - // set isLocalPlayer earlier, in case OnStartLocalplayer is called - // AFTER OnStartClient, in which case it would still be falsse here. - // many projects will check isLocalPlayer in OnStartClient though. - // TODO ideally set isLocalPlayer when NetworkClient.localPlayer is set? - if (NetworkClient.localPlayer == this) + if (isClient) { - isLocalPlayer = true; - } - - // If the instance/net ID is invalid here then this is an object instantiated from a prefab and the server should assign a valid ID - // NOTE: this might not be necessary because the above m_IsServer - // check already checks netId. BUT this case here checks only - // netId, so it would still check cases where isServer=false - // but netId!=0. - if (netId != 0) - { - // This object has already been spawned, this method might be called again - // if we try to respawn all objects. This can happen when we add a scene - // in that case there is nothing else to do. - return; - } - - netId = GetNextNetworkId(); - observers = new Dictionary(); - - //Debug.Log($"OnStartServer {this} NetId:{netId} SceneId:{sceneId:X}"); - - // add to spawned (note: the original EnableIsServer isn't needed - // because we already set m_isServer=true above) - NetworkServer.spawned[netId] = this; - - // in host mode we set isClient true before calling OnStartServer, - // otherwise isClient is false in OnStartServer. - if (NetworkClient.active) - { - isClient = true; + // ServerChangeScene doesn't send destroy messages. + // some identities may persist in DDOL. + // some are destroyed by scene change. + // if an identity is still in .owned remove it. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3308 + if (NetworkClient.connection != null) + NetworkClient.connection.owned.Remove(this); + + // if an identity is still in .spawned, remove it too. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3324 + NetworkClient.spawned.Remove(netId); } + } + internal void OnStartServer() + { foreach (NetworkBehaviour comp in NetworkBehaviours) { // an exception in OnStartServer should be caught, so that one @@ -702,20 +673,9 @@ internal void OnStopServer() bool clientStarted; internal void OnStartClient() { - if (clientStarted) - return; - clientStarted = true; - - isClient = true; + if (clientStarted) return; - // set isLocalPlayer earlier, in case OnStartLocalplayer is called - // AFTER OnStartClient, in which case it would still be falsse here. - // many projects will check isLocalPlayer in OnStartClient though. - // TODO ideally set isLocalPlayer when NetworkClient.localPlayer is set? - if (NetworkClient.localPlayer == this) - { - isLocalPlayer = true; - } + clientStarted = true; // Debug.Log($"OnStartClient {gameObject} netId:{netId}"); foreach (NetworkBehaviour comp in NetworkBehaviours) @@ -739,6 +699,10 @@ internal void OnStartClient() internal void OnStopClient() { + // In case this object was destroyed already don't call + // OnStopClient if OnStartClient hasn't been called. + if (!clientStarted) return; + foreach (NetworkBehaviour comp in NetworkBehaviours) { // an exception in OnStopClient should be caught, so that @@ -757,26 +721,33 @@ internal void OnStopClient() } } - // TODO any way to make this not static? - // introduced in https://github.com/vis2k/Mirror/commit/c7530894788bb843b0f424e8f25029efce72d8ca#diff-dc8b7a5a67840f75ccc884c91b9eb76ab7311c9ca4360885a7e41d980865bdc2 - // for PR https://github.com/vis2k/Mirror/pull/1263 - // - // explanation: - // we send the spawn message multiple times. Whenever an object changes - // authority, we send the spawn message again for the object. This is - // necessary because we need to reinitialize all variables when - // ownership change due to sync to owner feature. - // Without this static, the second time we get the spawn message we - // would call OnStartLocalPlayer again on the same object internal static NetworkIdentity previousLocalPlayer = null; internal void OnStartLocalPlayer() { + // ensure OnStartLocalPlayer is only called once. + // Room demo would call it multiple times: + // - once from ApplySpawnPayload + // - once from OnObjectSpawnFinished + // + // to reproduce: + // - open room demo, add the 3 scenes to build settings + // - add OnStartLocalPlayer log to RoomPlayer prefab + // - build, run server-only + // - in editor, connect, press ready + // - in server, start game + // - notice multiple OnStartLocalPlayer logs in editor client + // + // explanation: + // we send the spawn message multiple times. Whenever an object changes + // authority, we send the spawn message again for the object. This is + // necessary because we need to reinitialize all variables when + // ownership change due to sync to owner feature. + // Without this static, the second time we get the spawn message we + // would call OnStartLocalPlayer again on the same object if (previousLocalPlayer == this) return; previousLocalPlayer = this; - isLocalPlayer = true; - foreach (NetworkBehaviour comp in NetworkBehaviours) { // an exception in OnStartLocalPlayer should be caught, so that @@ -815,150 +786,264 @@ internal void OnStopLocalPlayer() } } - bool hadAuthority; - internal void NotifyAuthority() + // build dirty mask for server owner & observers (= all dirty components). + // faster to do it in one iteration instead of iterating separately. + (ulong, ulong) ServerDirtyMasks(bool initialState) { - if (!hadAuthority && hasAuthority) - OnStartAuthority(); - if (hadAuthority && !hasAuthority) - OnStopAuthority(); - hadAuthority = hasAuthority; + ulong ownerMask = 0; + ulong observerMask = 0; + + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour component = components[i]; + + bool dirty = component.IsDirty(); + ulong nthBit = (1u << i); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for initial, it should always sync owner. + // for delta, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty)) + ownerMask |= nthBit; + + // observers need to be considered only in Observers mode + // + // for initial, it should always sync to observers. + // for delta, only if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (component.syncMode == SyncMode.Observers && (initialState || dirty)) + observerMask |= nthBit; + } + + return (ownerMask, observerMask); } - internal void OnStartAuthority() + // build dirty mask for client. + // server always knows initialState, so we don't need it here. + ulong ClientDirtyMask() { - foreach (NetworkBehaviour comp in NetworkBehaviours) + ulong mask = 0; + + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) { - // an exception in OnStartAuthority should be caught, so that one - // component's exception doesn't stop all other components from - // being initialized - // => this is what Unity does for Start() etc. too. - // one exception doesn't stop all the other Start() calls! - try - { - comp.OnStartAuthority(); - } - catch (Exception e) + // on the client, we need to consider different sync scenarios: + // + // ServerToClient SyncDirection: + // do nothing. + // ClientToServer SyncDirection: + // serialize only if owned. + + // on client, only consider owned components with SyncDirection to server + NetworkBehaviour component = components[i]; + if (isOwned && component.syncDirection == SyncDirection.ClientToServer) { - Debug.LogException(e, comp); + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + if (component.IsDirty()) mask |= (1u << i); } } + + return mask; } - internal void OnStopAuthority() + // check if n-th component is dirty. + // in other words, if it has the n-th bit set in the dirty mask. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirty(ulong mask, int index) { - foreach (NetworkBehaviour comp in NetworkBehaviours) + ulong nthBit = (ulong)(1 << index); + return (mask & nthBit) != 0; + } + + // serialize components into writer on the server. + // check ownerWritten/observersWritten to know if anything was written + // We pass dirtyComponentsMask into this function so that we can check + // if any Components are dirty before creating writers + internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // check which components are dirty for owner / observers. + // this is quite complicated with SyncMode + SyncDirection. + // see the function for explanation. + // + // instead of writing a 1 byte index per component, + // we limit components to 64 bits and write one ulong instead. + // the ulong is also varint compressed for minimum bandwidth. + (ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState); + + // if nothing dirty, then don't even write the mask. + // otherwise, every unchanged object would send a 1 byte dirty mask! + if (ownerMask != 0) Compression.CompressVarUInt(ownerWriter, ownerMask); + if (observerMask != 0) Compression.CompressVarUInt(observersWriter, observerMask); + + // serialize all components + // perf: only iterate if either dirty mask has dirty bits. + if ((ownerMask | observerMask) != 0) { - // an exception in OnStopAuthority should be caught, so that one - // component's exception doesn't stop all other components from - // being initialized - // => this is what Unity does for Start() etc. too. - // one exception doesn't stop all the other Start() calls! - try - { - comp.OnStopAuthority(); - } - catch (Exception e) + for (int i = 0; i < components.Length; ++i) { - Debug.LogException(e, comp); + NetworkBehaviour comp = components[i]; + + // is the component dirty for anyone (owner or observers)? + // may be serialized to owner, observer, both, or neither. + // + // OnSerialize should only be called once. + // this is faster, and it cleaner because it may set + // internal state, counters, logs, etc. + // + // previously we always serialized to owner and then copied + // the serialization to observers. however, since + // SyncDirection it's not guaranteed to be in owner anymore. + // so we need to serialize to temporary writer first. + // and then copy as needed. + bool ownerDirty = IsDirty(ownerMask, i); + bool observersDirty = IsDirty(observerMask, i); + if (ownerDirty || observersDirty) + { + // serialize into helper writer + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, initialState); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirty) ownerWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); + } + } } } } - // vis2k: readstring bug prevention: https://github.com/vis2k/Mirror/issues/2617 - // -> OnSerialize writes length,componentData,length,componentData,... - // -> OnDeserialize carefully extracts each data, then deserializes each component with separate readers - // -> it will be impossible to read too many or too few bytes in OnDeserialize - // -> we can properly track down errors - bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initialState) + // serialize components into writer on the client. + internal void SerializeClient(NetworkWriter writer) { - // write placeholder length bytes - // (jumping back later is WAY faster than allocating a temporary - // writer for the payload, then writing payload.size, payload) - int headerPosition = writer.Position; - // no varint because we don't know the final size yet - writer.WriteInt(0); - int contentPosition = writer.Position; - - // write payload - bool result = false; - try - { - result = comp.OnSerialize(writer, initialState); - } - catch (Exception e) - { - // show a detailed error and let the user know what went wrong - Debug.LogError($"OnSerialize failed for: object={name} component={comp.GetType()} sceneId={sceneId:X}\n\n{e}"); - } - int endPosition = writer.Position; + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; - // fill in length now - writer.Position = headerPosition; - writer.WriteInt(endPosition - contentPosition); - writer.Position = endPosition; + // check which components are dirty. + // this is quite complicated with SyncMode + SyncDirection. + // see the function for explanation. + // + // instead of writing a 1 byte index per component, + // we limit components to 64 bits and write one ulong instead. + // the ulong is also varint compressed for minimum bandwidth. + ulong dirtyMask = ClientDirtyMask(); + + // varint compresses the mask to 1 byte in most cases. + // instead of writing an 8 byte ulong. + // 7 components fit into 1 byte. (previously 7 bytes) + // 11 components fit into 2 bytes. (previously 11 bytes) + // 16 components fit into 3 bytes. (previously 16 bytes) + // TODO imer: server knows amount of comps, write N bytes instead + + // if nothing dirty, then don't even write the mask. + // otherwise, every unchanged object would send a 1 byte dirty mask! + if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask); - //Debug.Log($"OnSerializeSafely written for object {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}"); + // serialize all components + // perf: only iterate if dirty mask has dirty bits. + if (dirtyMask != 0) + { + // serialize all components + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour comp = components[i]; - return result; + // is this component dirty? + // reuse the mask instead of calling comp.IsDirty() again here. + if (IsDirty(dirtyMask, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + // serialize into writer. + // server always knows initialState, we never need to send it + comp.Serialize(writer, false); + } + } + } } - // serialize all components using dirtyComponentsMask - // check ownerWritten/observersWritten to know if anything was written - // We pass dirtyComponentsMask into this function so that we can check - // if any Components are dirty before creating writers - internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) + // deserialize components from the client on the server. + // there's no 'initialState'. server always knows the initial state. + internal bool DeserializeServer(NetworkReader reader) { - // check if components are in byte.MaxRange just to be 100% sure - // that we avoid overflows + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); NetworkBehaviour[] components = NetworkBehaviours; - if (components.Length > byte.MaxValue) - throw new IndexOutOfRangeException($"{name} has more than {byte.MaxValue} components. This is not supported."); - // serialize all components + // first we deserialize the varinted dirty mask + ulong mask = Compression.DecompressVarUInt(reader); + + // now deserialize every dirty component for (int i = 0; i < components.Length; ++i) { - // is this component dirty? - // -> always serialize if initialState so all components are included in spawn packet - // -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet - NetworkBehaviour comp = components[i]; - if (initialState || comp.IsDirty()) + // was this one dirty? + if (IsDirty(mask, i)) { - //Debug.Log($"OnSerializeAllSafely: {name} -> {comp.GetType()} initial:{ initialState}"); - - // remember start position in case we need to copy it into - // observers writer too - int startPosition = ownerWriter.Position; - - // write index as byte [0..255] - ownerWriter.WriteByte((byte)i); - - // serialize into ownerWriter first - // (owner always gets everything!) - OnSerializeSafely(comp, ownerWriter, initialState); - - // copy into observersWriter too if SyncMode.Observers - // -> we copy instead of calling OnSerialize again because - // we don't know what magic the user does in OnSerialize. - // -> it's not guaranteed that calling it twice gets the - // same result - // -> it's not guaranteed that calling it twice doesn't mess - // with the user's OnSerialize timing code etc. - // => so we just copy the result without touching - // OnSerialize again - if (comp.syncMode == SyncMode.Observers) + NetworkBehaviour comp = components[i]; + + // safety check to ensure clients can only modify their own + // ClientToServer components, nothing else. + if (comp.syncDirection == SyncDirection.ClientToServer) { - ArraySegment segment = ownerWriter.ToArraySegment(); - int length = ownerWriter.Position - startPosition; - observersWriter.WriteBytes(segment.Array, startPosition, length); + // deserialize this component + // server always knows the initial state (initial=false) + // disconnect if failed, to prevent exploits etc. + if (!comp.Deserialize(reader, false)) return false; + + // server received state from the owner client. + // set dirty so it's broadcast to other clients too. + // + // note that we set the _whole_ component as dirty. + // everything will be broadcast to others. + // SetSyncVarDirtyBits() would be nicer, but not all + // components use [SyncVar]s. + comp.SetDirty(); } } } + + // successfully deserialized everything + return true; } - // get cached serialization for this tick (or serialize if none yet) - // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - internal NetworkIdentitySerialization GetSerializationAtTick(int tick) + // deserialize components from server on the client. + internal void DeserializeClient(NetworkReader reader, bool initialState) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // first we deserialize the varinted dirty mask + ulong mask = Compression.DecompressVarUInt(reader); + + // now deserialize every dirty component + for (int i = 0; i < components.Length; ++i) + { + // was this one dirty? + if (IsDirty(mask, i)) + { + // deserialize this component + NetworkBehaviour comp = components[i]; + comp.Deserialize(reader, initialState); + } + } + } + + // get cached serialization for this tick (or serialize if none yet). + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks. + // calls SerializeServer, so this function is to be called on server. + internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick) { // only rebuild serialization once per tick. reuse otherwise. // except for tests, where Time.frameCount never increases. @@ -966,16 +1051,20 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick) // (otherwise [SyncVar] changes would never be serialized in tests) // // NOTE: != instead of < because int.max+1 overflows at some point. - if (lastSerialization.tick != tick || !Application.isPlaying) + if (lastSerialization.tick != tick +#if UNITY_EDITOR + || !Application.isPlaying +#endif + ) { // reset lastSerialization.ownerWriter.Position = 0; lastSerialization.observersWriter.Position = 0; // serialize - OnSerializeAllSafely(false, - lastSerialization.ownerWriter, - lastSerialization.observersWriter); + SerializeServer(false, + lastSerialization.ownerWriter, + lastSerialization.observersWriter); // clear dirty bits for the components that we serialized. // previously we did this in NetworkServer.BroadcastToConnection @@ -984,7 +1073,7 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick) // 'lastSerialization.tick != tick' scope. // so only do it once. // - // NOTE: not in OnSerializeAllSafely as that should only do one + // NOTE: not in Serializell as that should only do one // thing: serialize data. // // @@ -1007,101 +1096,25 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick) return lastSerialization; } - void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState) - { - // read header as 4 bytes and calculate this chunk's start+end - int contentSize = reader.ReadInt(); - int chunkStart = reader.Position; - int chunkEnd = reader.Position + contentSize; - - // call OnDeserialize and wrap it in a try-catch block so there's no - // way to mess up another component's deserialization - try - { - //Debug.Log($"OnDeserializeSafely: {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} length:{contentSize}"); - comp.OnDeserialize(reader, initialState); - } - catch (Exception e) - { - // show a detailed error and let the user know what went wrong - Debug.LogError($"OnDeserialize failed Exception={e.GetType()} (see below) object={name} component={comp.GetType()} sceneId={sceneId:X} length={contentSize}. Possible Reasons:\n" + - $" * Do {comp.GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data({contentSize} bytes)? \n" + - $" * Was there an exception in {comp.GetType()}'s OnSerialize/OnDeserialize code?\n" + - $" * Are the server and client the exact same project?\n" + - $" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" + - $"Exception {e}"); - } - - // now the reader should be EXACTLY at 'before + size'. - // otherwise the component read too much / too less data. - if (reader.Position != chunkEnd) - { - // warn the user - int bytesRead = reader.Position - chunkStart; - Debug.LogWarning($"OnDeserialize was expected to read {contentSize} instead of {bytesRead} bytes for object:{name} component={comp.GetType()} sceneId={sceneId:X}. Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); - - // fix the position, so the following components don't all fail - reader.Position = chunkEnd; - } - } - - internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState) + // Clear only dirty component's dirty bits. ignores components which + // may be dirty but not ready to be synced yet (because of syncInterval) + // + // NOTE: this used to be very important to avoid ever + // growing SyncList changes if they had no observers, + // but we've added SyncObject.isRecording since. + internal void ClearDirtyComponentsDirtyBits() { - if (NetworkBehaviours == null) - { - Debug.LogError($"NetworkBehaviours array is null on {gameObject.name}!\n" + - $"Typically this can happen when a networked object is a child of a " + - $"non-networked parent that's disabled, preventing Awake on the networked object " + - $"from being invoked, where the NetworkBehaviours array is initialized.", gameObject); - return; - } - - // deserialize all components that were received - NetworkBehaviour[] components = NetworkBehaviours; - while (reader.Remaining > 0) + foreach (NetworkBehaviour comp in NetworkBehaviours) { - // read & check index [0..255] - byte index = reader.ReadByte(); - if (index < components.Length) + if (comp.IsDirty()) { - // deserialize this component - OnDeserializeSafely(components[index], reader, initialState); + comp.ClearAllDirtyBits(); } } } - // Helper function to handle Command/Rpc - internal void HandleRemoteCall(byte componentIndex, int functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null) - { - // check if unity object has been destroyed - if (this == null) - { - Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]"); - return; - } - - // find the right component to invoke the function on - if (componentIndex >= NetworkBehaviours.Length) - { - Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]"); - return; - } - - NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex]; - if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection)) - { - Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}]."); - } - } - internal void AddObserver(NetworkConnectionToClient conn) { - if (observers == null) - { - Debug.LogError($"AddObserver for {gameObject} observer list is null"); - return; - } - if (observers.ContainsKey(conn.connectionId)) { // if we try to add a connectionId that was already added, then @@ -1139,23 +1152,19 @@ internal void AddObserver(NetworkConnectionToClient conn) conn.AddToObserving(this); } - // this is used when a connection is destroyed, since the "observers" property is read-only - internal void RemoveObserver(NetworkConnection conn) + // clear all component's dirty bits no matter what + internal void ClearAllComponentsDirtyBits() { - observers?.Remove(conn.connectionId); + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + comp.ClearAllDirtyBits(); + } } - // Called when NetworkIdentity is destroyed - internal void ClearObservers() + // this is used when a connection is destroyed, since the "observers" property is read-only + internal void RemoveObserver(NetworkConnection conn) { - if (observers != null) - { - foreach (NetworkConnectionToClient conn in observers.Values) - { - conn.RemoveFromObserving(this, true); - } - observers.Clear(); - } + observers.Remove(conn.connectionId); } /// Assign control of an object to a client via the client's NetworkConnection. @@ -1197,6 +1206,20 @@ public bool AssignClientAuthority(NetworkConnectionToClient conn) return true; } + // used when adding players + internal void SetClientOwner(NetworkConnectionToClient conn) + { + // do nothing if it already has an owner + if (connectionToClient != null && conn != connectionToClient) + { + Debug.LogError($"Object {this} netId={netId} already has an owner. Use RemoveClientAuthority() first", this); + return; + } + + // otherwise set the owner connection + connectionToClient = conn; + } + /// Removes ownership for an object. // Applies to objects that had authority set by AssignClientAuthority, // or NetworkServer.Spawn with a NetworkConnection parameter included. @@ -1232,6 +1255,10 @@ public void RemoveClientAuthority() // we can't destroy them (they are always in the scene). // instead we disable them and call Reset(). // + // Do not reset SyncObjects from Reset + // - Unspawned objects need to retain their list contents + // - They may be respawned, especially players, but others as well. + // // OLD COMMENT: // Marks the identity for future reset, this is because we cant reset // the identity during destroy as people might want to be able to read @@ -1239,9 +1266,6 @@ public void RemoveClientAuthority() // after OnDestroy is called. internal void Reset() { - // make sure to call this before networkBehavioursCache is cleared below - ResetSyncObjects(); - hasSpawned = false; clientStarted = false; isClient = false; @@ -1249,7 +1273,7 @@ internal void Reset() //isLocalPlayer = false; <- cleared AFTER ClearLocalPlayer below! // remove authority flag. This object may be unspawned, not destroyed, on client. - hasAuthority = false; + isOwned = false; NotifyAuthority(); netId = 0; @@ -1273,45 +1297,64 @@ internal void Reset() isLocalPlayer = false; } - // clear all component's dirty bits no matter what - internal void ClearAllComponentsDirtyBits() + bool hadAuthority; + internal void NotifyAuthority() + { + if (!hadAuthority && isOwned) + OnStartAuthority(); + if (hadAuthority && !isOwned) + OnStopAuthority(); + hadAuthority = isOwned; + } + + internal void OnStartAuthority() { foreach (NetworkBehaviour comp in NetworkBehaviours) { - comp.ClearAllDirtyBits(); + // an exception in OnStartAuthority should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStartAuthority(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } } } - // Clear only dirty component's dirty bits. ignores components which - // may be dirty but not ready to be synced yet (because of syncInterval) - // - // NOTE: this used to be very important to avoid ever - // growing SyncList changes if they had no observers, - // but we've added SyncObject.isRecording since. - internal void ClearDirtyComponentsDirtyBits() + internal void OnStopAuthority() { foreach (NetworkBehaviour comp in NetworkBehaviours) { - if (comp.IsDirty()) + // an exception in OnStopAuthority should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try { - comp.ClearAllDirtyBits(); + comp.OnStopAuthority(); + } + catch (Exception e) + { + Debug.LogException(e, comp); } } } - void ResetSyncObjects() + // Called when NetworkIdentity is destroyed + internal void ClearObservers() { - // ResetSyncObjects is called by Reset, which is called by Unity. - // AddComponent() calls Reset(). - // AddComponent() is called before Awake(). - // so NetworkBehaviours may not be initialized yet. - if (NetworkBehaviours == null) - return; - - foreach (NetworkBehaviour comp in NetworkBehaviours) + foreach (NetworkConnectionToClient conn in observers.Values) { - comp.ResetSyncObjects(); + conn.RemoveFromObserving(this, true); } + observers.Clear(); } } } diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs.meta b/Assets/Mirror/Core/NetworkIdentity.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkIdentity.cs.meta rename to Assets/Mirror/Core/NetworkIdentity.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs b/Assets/Mirror/Core/NetworkLoop.cs similarity index 91% rename from Assets/Mirror/Runtime/NetworkLoop.cs rename to Assets/Mirror/Core/NetworkLoop.cs index 50d9e95..d341b4b 100644 --- a/Assets/Mirror/Runtime/NetworkLoop.cs +++ b/Assets/Mirror/Core/NetworkLoop.cs @@ -26,17 +26,8 @@ // to the beginning of PostLateUpdate doesn't actually work. using System; using UnityEngine; - -// PlayerLoop and LowLevel were in the Experimental namespace until 2019.3 -// https://docs.unity3d.com/2019.2/Documentation/ScriptReference/Experimental.LowLevel.PlayerLoop.html -// https://docs.unity3d.com/2019.3/Documentation/ScriptReference/LowLevel.PlayerLoop.html -#if UNITY_2019_3_OR_NEWER using UnityEngine.LowLevel; using UnityEngine.PlayerLoop; -#else -using UnityEngine.Experimental.LowLevel; -using UnityEngine.Experimental.PlayerLoop; -#endif namespace Mirror { @@ -45,7 +36,7 @@ public static class NetworkLoop // helper enum to add loop to begin/end of subSystemList internal enum AddMode { Beginning, End } - // callbacks in case someone needs to use early/lateupdate too. + // callbacks for others to hook into if they need Early/LateUpdate. public static Action OnEarlyUpdate; public static Action OnLateUpdate; @@ -69,7 +60,7 @@ internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction fun // recursively keep looking if (playerLoop.subSystemList != null) { - for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + for (int i = 0; i < playerLoop.subSystemList.Length; ++i) { int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType); if (index != -1) return index; @@ -128,7 +119,6 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T // shift to the right, write into first array element Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1); playerLoop.subSystemList[0] = system; - } // append our custom loop to the end else if (addMode == AddMode.End) @@ -148,7 +138,7 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T // recursively keep looking if (playerLoop.subSystemList != null) { - for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + for (int i = 0; i < playerLoop.subSystemList.Length; ++i) { if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode)) return true; @@ -167,12 +157,7 @@ static void RuntimeInitializeOnLoad() // 2019 has GetCURRENTPlayerLoop which is safe to use without // breaking other custom system's custom loops. // see also: https://github.com/vis2k/Mirror/pull/2627/files - PlayerLoopSystem playerLoop = -#if UNITY_2019_3_OR_NEWER - PlayerLoop.GetCurrentPlayerLoop(); -#else - PlayerLoop.GetDefaultPlayerLoop(); -#endif + PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop(); // add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after // any Unity initializations but before the first Update/FixedUpdate diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs.meta b/Assets/Mirror/Core/NetworkLoop.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkLoop.cs.meta rename to Assets/Mirror/Core/NetworkLoop.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs similarity index 85% rename from Assets/Mirror/Runtime/NetworkManager.cs rename to Assets/Mirror/Core/NetworkManager.cs index 37be9ae..f24d8ce 100644 --- a/Assets/Mirror/Runtime/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using kcp2k; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Serialization; @@ -29,13 +28,37 @@ public class NetworkManager : MonoBehaviour public bool runInBackground = true; /// Should the server auto-start when 'Server Build' is checked in build settings + [Header("Headless Builds")] [Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")] [FormerlySerializedAs("startOnHeadless")] public bool autoStartServerBuild = true; + [Tooltip("Automatically connect the client in headless builds. Useful for CCU tests with bot clients.\n\nAddress may be passed as command line argument.\n\nMake sure that only 'autostartServer' or 'autoconnectClient' is enabled, not both!")] + public bool autoConnectClientBuild; + /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. - [Tooltip("Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] - public int serverTickRate = 30; + [Tooltip("Server & Client send rate per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] + [FormerlySerializedAs("serverTickRate")] + public int sendRate = 30; + + // Deprecated 2022-10-31 + [Obsolete("NetworkManager.serverTickRate was renamed to sendRate because that's what it configures for both server & client now.")] + public int serverTickRate => sendRate; + + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + // Deprecated 2022-10-06 + [Obsolete("NetworkManager.serverTickInterval was moved to NetworkServer.tickInterval for consistency.")] + public float serverTickInterval => NetworkServer.tickInterval; + + // client send rate follows server send rate to avoid errors for now + /// Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + // [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] + // public int clientSendRate = 30; // 33 ms /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). [Header("Scene Management")] @@ -53,8 +76,7 @@ public class NetworkManager : MonoBehaviour // transport layer [Header("Network Info")] [Tooltip("Transport component attached to this object that server and client will use to connect")] - [SerializeField] - protected Transport transport; + public Transport transport; /// Server's address for clients to connect to. [FormerlySerializedAs("m_NetworkAddress")] @@ -96,6 +118,9 @@ public class NetworkManager : MonoBehaviour public static List startPositions = new List(); public static int startPositionIndex; + [Header("Debug")] + public bool timeInterpolationGui = false; + /// The one and only NetworkManager public static NetworkManager singleton { get; internal set; } @@ -128,7 +153,7 @@ public virtual void OnValidate() // always >= 0 maxConnections = Mathf.Max(maxConnections, 0); - if (playerPrefab != null && playerPrefab.GetComponent() == null) + if (playerPrefab != null && !playerPrefab.TryGetComponent(out NetworkIdentity _)) { Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); playerPrefab = null; @@ -161,24 +186,6 @@ public virtual void Reset() return; } } - - // add transport if there is none yet. makes upgrading easier. - if (transport == null) - { -#if UNITY_EDITOR - // RecordObject needs to be called before we make the change - UnityEditor.Undo.RecordObject(gameObject, "Added default Transport"); -#endif - - transport = GetComponent(); - - // was a transport added yet? if not, add one - if (transport == null) - { - transport = gameObject.AddComponent(); - Debug.Log("NetworkManager: added default Transport because there was none yet."); - } - } } // virtual so that inheriting classes' Awake() can call base.Awake() too @@ -187,7 +194,8 @@ public virtual void Awake() // Don't allow collision-destroyed second instance to continue. if (!InitializeSingleton()) return; - Debug.Log("Mirror | mirror-networking.com | discord.gg/N9QVxbM"); + // Apply configuration in Awake once already + ApplyConfiguration(); // Set the networkSceneName to prevent a scene reload // if client connection to server fails. @@ -210,26 +218,45 @@ public virtual void Start() { StartServer(); } + // only start server or client, never both + else if(autoConnectClientBuild) + { + StartClient(); + } #endif } - // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too - public virtual void LateUpdate() + // make sure to call base.Update() when overwriting + public virtual void Update() { - UpdateScene(); + ApplyConfiguration(); } - // keep the online scene change check in a separate function - bool IsServerOnlineSceneChangeNeeded() + // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too + public virtual void LateUpdate() { - // Only change scene if the requested online scene is not blank, and is not already loaded - return !string.IsNullOrWhiteSpace(onlineScene) && !IsSceneActive(onlineScene) && onlineScene != offlineScene; + UpdateScene(); } - public static bool IsSceneActive(string scene) + // keep the online scene change check in a separate function. + // only change scene if the requested online scene is not blank, and is not already loaded. + bool IsServerOnlineSceneChangeNeeded() => + !string.IsNullOrWhiteSpace(onlineScene) && + !Utils.IsSceneActive(onlineScene) && + onlineScene != offlineScene; + + // Deprecated 2022-12-12 + [Obsolete("NetworkManager.IsSceneActive moved to Utils.IsSceneActive")] + public static bool IsSceneActive(string scene) => Utils.IsSceneActive(scene); + + // NetworkManager exposes some NetworkServer/Client configuration. + // we apply it every Update() in order to avoid two sources of truth. + // fixes issues where NetworkServer.sendRate was never set because + // NetworkManager.StartServer was never called, etc. + // => all exposed settings should be applied at all times if NM exists. + void ApplyConfiguration() { - Scene activeScene = SceneManager.GetActiveScene(); - return activeScene.path == scene || activeScene.name == scene; + NetworkServer.tickRate = sendRate; } // full server setup code, without spawning objects yet @@ -252,18 +279,11 @@ void SetupServer() // start listening to network connections NetworkServer.Listen(maxConnections); - // call OnStartServer AFTER Listen, so that NetworkServer.active is - // true and we can call NetworkServer.Spawn in OnStartServer - // overrides. - // (useful for loading & spawning stuff from database etc.) - // - // note: there is no risk of someone connecting after Listen() and - // before OnStartServer() because this all runs in one thread - // and we don't start processing connects until Update. - OnStartServer(); - // this must be after Listen(), since that registers the default message handlers RegisterServerMessages(); + + // do not call OnStartServer here yet. + // this is up to the caller. different for server-only vs. host mode. } /// Starts the server, listening for incoming connections. @@ -295,6 +315,16 @@ public void StartServer() SetupServer(); + // call OnStartServer AFTER Listen, so that NetworkServer.active is + // true and we can call NetworkServer.Spawn in OnStartServer + // overrides. + // (useful for loading & spawning stuff from database etc.) + // + // note: there is no risk of someone connecting after Listen() and + // before OnStartServer() because this all runs in one thread + // and we don't start processing connects until Update. + OnStartServer(); + // scene change needed? then change scene and spawn afterwards. if (IsServerOnlineSceneChangeNeeded()) { @@ -307,17 +337,8 @@ public void StartServer() } } - /// Starts the client, connects it to the server with networkAddress. - public void StartClient() + void SetupClient() { - if (NetworkClient.active) - { - Debug.LogWarning("Client already started."); - return; - } - - mode = NetworkManagerMode.ClientOnly; - InitializeSingleton(); if (runInBackground) @@ -329,6 +350,22 @@ public void StartClient() authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); } + // NetworkClient.sendRate = clientSendRate; + } + + /// Starts the client, connects it to the server with networkAddress. + public void StartClient() + { + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } + + mode = NetworkManagerMode.ClientOnly; + + SetupClient(); + // In case this is a headless client... ConfigureHeadlessFrameRate(); @@ -357,16 +394,7 @@ public void StartClient(Uri uri) mode = NetworkManagerMode.ClientOnly; - InitializeSingleton(); - - if (runInBackground) - Application.runInBackground = true; - - if (authenticator != null) - { - authenticator.OnStartClient(); - authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); - } + SetupClient(); RegisterClientMessages(); @@ -413,11 +441,6 @@ public void StartHost() // setup server first SetupServer(); - // call OnStartHost AFTER SetupServer. this way we can use - // NetworkServer.Spawn etc. in there too. just like OnStartServer - // is called after the server is actually properly started. - OnStartHost(); - // scene change needed? then change scene and spawn afterwards. // => BEFORE host client connects. if client auth succeeds then the // server tells it to load 'onlineScene'. we can't do that if @@ -474,6 +497,21 @@ void FinishStartHost() // TODO call this after spawnobjects and worry about the syncvar hook fix later? NetworkClient.ConnectHost(); + // invoke user callbacks AFTER ConnectHost has set .activeHost. + // this way initialization can properly handle host mode. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3302 + // where [SyncVar] hooks wouldn't be called for objects spawned in + // NetworkManager.OnStartServer, because .activeHost was still false. + // + // TODO is there a risk of someone connecting between Listen() and FinishStartHost()? + OnStartServer(); + + // call OnStartHost AFTER SetupServer. this way we can use + // NetworkServer.Spawn etc. in there too. just like OnStartServer + // is called after the server is actually properly started. + OnStartHost(); + // server scene was loaded. now spawn all the objects NetworkServer.SpawnObjects(); @@ -482,26 +520,14 @@ void FinishStartHost() // DO NOT do this earlier. it would cause race conditions where a // client will do things before the server is even fully started. //Debug.Log("StartHostClient called"); - StartHostClient(); - } - - void StartHostClient() - { - //Debug.Log("NetworkManager ConnectLocalClient"); - - if (authenticator != null) - { - authenticator.OnStartClient(); - authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); - } + SetupClient(); networkAddress = "localhost"; - NetworkServer.ActivateHostScene(); RegisterClientMessages(); - // ConnectLocalServer needs to be called AFTER RegisterClientMessages + // call OnConencted needs to be called AFTER RegisterClientMessages // (https://github.com/vis2k/Mirror/pull/1249/) - NetworkClient.ConnectLocalServer(); + HostMode.InvokeOnConnected(); OnStartClient(); } @@ -570,45 +596,22 @@ public void StopClient() if (mode == NetworkManagerMode.Offline) return; - if (authenticator != null) - { - authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); - authenticator.OnStopClient(); - } - - // Get Network Manager out of DDOL before going to offline scene - // to avoid collision and let a fresh Network Manager be created. - // IMPORTANT: .gameObject can be null if StopClient is called from - // OnApplicationQuit or from tests! - if (gameObject != null - && gameObject.scene.name == "DontDestroyOnLoad" - && !string.IsNullOrWhiteSpace(offlineScene) - && SceneManager.GetActiveScene().path != offlineScene) - SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); - - OnStopClient(); - - //Debug.Log("NetworkManager StopClient"); - - // set offline mode BEFORE changing scene so that FinishStartScene - // doesn't think we need initialize anything. - // set offline mode BEFORE NetworkClient.Disconnect so StopClient - // only runs once. - mode = NetworkManagerMode.Offline; - - // shutdown client + // ask client -> transport to disconnect. + // handle voluntary and involuntary disconnects in OnClientDisconnect. + // + // StopClient + // NetworkClient.Disconnect + // Transport.Disconnect + // ... + // Transport.OnClientDisconnect + // NetworkClient.OnTransportDisconnect + // NetworkManager.OnClientDisconnect NetworkClient.Disconnect(); - NetworkClient.Shutdown(); - // If this is the host player, StopServer will already be changing scenes. - // Check loadingSceneAsync to ensure we don't double-invoke the scene change. - // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. - if (!string.IsNullOrWhiteSpace(offlineScene) && !IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) - { - ClientChangeScene(offlineScene, SceneOperation.Normal); - } - - networkSceneName = ""; + // UNET invoked OnDisconnected cleanup immediately. + // let's keep it for now, in case any projects depend on it. + // TODO simply remove this in the future. + OnClientDisconnectInternal(); } // called when quitting the application by closing the window / pressing @@ -642,7 +645,7 @@ public virtual void OnApplicationQuit() public virtual void ConfigureHeadlessFrameRate() { #if UNITY_SERVER - Application.targetFrameRate = serverTickRate; + Application.targetFrameRate = sendRate; // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); #endif } @@ -680,7 +683,7 @@ bool InitializeSingleton() // set active transport AFTER setting singleton. // so only if we didn't destroy ourselves. - Transport.activeTransport = transport; + Transport.active = transport; return true; } @@ -781,7 +784,10 @@ public virtual void ServerChangeScene(string newSceneName) if (NetworkServer.active) { // notify all clients about the new scene - NetworkServer.SendToAll(new SceneMessage { sceneName = newSceneName }); + NetworkServer.SendToAll(new SceneMessage + { + sceneName = newSceneName + }); } startPositionIndex = 0; @@ -955,10 +961,6 @@ void FinishLoadSceneHost() if (clientReadyConnection != null) { -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientConnect(clientReadyConnection); -#pragma warning restore 618 clientLoadedScene = true; clientReadyConnection = null; } @@ -992,13 +994,7 @@ void FinishLoadSceneHost() OnServerSceneChanged(networkSceneName); if (NetworkClient.isConnected) - { - // let client know that we changed scene -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 - } + OnClientSceneChanged(); } } @@ -1024,21 +1020,12 @@ void FinishLoadSceneClientOnly() if (clientReadyConnection != null) { -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientConnect(clientReadyConnection); -#pragma warning restore 618 clientLoadedScene = true; clientReadyConnection = null; } if (NetworkClient.isConnected) - { -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 - } + OnClientSceneChanged(); } /// @@ -1070,7 +1057,7 @@ public static void UnRegisterStartPosition(Transform start) } /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. - public Transform GetStartPosition() + public virtual Transform GetStartPosition() { // first remove any dead transforms startPositions.RemoveAll(t => t == null); @@ -1118,7 +1105,10 @@ void OnServerAuthenticated(NetworkConnectionToClient conn) // proceed with the login handshake by calling OnServerConnect if (networkSceneName != "" && networkSceneName != offlineScene) { - SceneMessage msg = new SceneMessage() { sceneName = networkSceneName }; + SceneMessage msg = new SceneMessage() + { + sceneName = networkSceneName + }; conn.Send(msg); } @@ -1141,7 +1131,7 @@ void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage return; } - if (autoCreatePlayer && playerPrefab.GetComponent() == null) + if (autoCreatePlayer && !playerPrefab.TryGetComponent(out NetworkIdentity _)) { Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); return; @@ -1180,39 +1170,86 @@ void OnClientAuthenticated() // set connection to authenticated NetworkClient.connection.isAuthenticated = true; - // proceed with the login handshake by calling OnClientConnect - if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || IsSceneActive(onlineScene)) + // Set flag to wait for scene change? + if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || Utils.IsSceneActive(onlineScene)) { clientLoadedScene = false; -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientConnect(NetworkClient.connection); -#pragma warning restore 618 } else { - // will wait for scene id to come from the server. + // Scene message expected from server. clientLoadedScene = true; clientReadyConnection = NetworkClient.connection; } + + // Call virtual method regardless of whether a scene change is expected or not. + OnClientConnect(); } + // Transport callback, invoked after client fully disconnected. + // the call order should always be: + // Disconnect() -> ask Transport -> Transport.OnDisconnected -> Cleanup void OnClientDisconnectInternal() { //Debug.Log("NetworkManager.OnClientDisconnectInternal"); -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientDisconnect(NetworkClient.connection); -#pragma warning restore 618 + + // Only let this run once. StopClient in Host mode changes to ServerOnly + if (mode == NetworkManagerMode.ServerOnly || mode == NetworkManagerMode.Offline) + return; + + // user callback + OnClientDisconnect(); + + if (authenticator != null) + { + authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); + authenticator.OnStopClient(); + } + + // set mode BEFORE changing scene so FinishStartScene doesn't re-initialize anything. + // set mode BEFORE NetworkClient.Disconnect so StopClient only runs once. + // set mode BEFORE OnStopClient so StopClient only runs once. + // If we got here from StopClient in Host mode, change to ServerOnly. + // - If StopHost was called, StopServer will put us in Offline mode. + if (mode == NetworkManagerMode.Host) + mode = NetworkManagerMode.ServerOnly; + else + mode = NetworkManagerMode.Offline; + + //Debug.Log("NetworkManager StopClient"); + OnStopClient(); + + // shutdown client + NetworkClient.Shutdown(); + + // Exit here if we're now in ServerOnly mode (StopClient called in Host mode). + if (mode == NetworkManagerMode.ServerOnly) return; + + // Get Network Manager out of DDOL before going to offline scene + // to avoid collision and let a fresh Network Manager be created. + // IMPORTANT: .gameObject can be null if StopClient is called from + // OnApplicationQuit or from tests! + if (gameObject != null + && gameObject.scene.name == "DontDestroyOnLoad" + && !string.IsNullOrWhiteSpace(offlineScene) + && SceneManager.GetActiveScene().path != offlineScene) + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + + // If StopHost called in Host mode, StopServer will change scenes after this. + // Check loadingSceneAsync to ensure we don't double-invoke the scene change. + // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. + if (!string.IsNullOrWhiteSpace(offlineScene) && !Utils.IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) + { + ClientChangeScene(offlineScene, SceneOperation.Normal); + } + + networkSceneName = ""; } void OnClientNotReadyMessageInternal(NotReadyMessage msg) { //Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); NetworkClient.ready = false; -#pragma warning disable 618 - OnClientNotReady(NetworkClient.connection); -#pragma warning restore 618 OnClientNotReady(); // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. @@ -1269,8 +1306,16 @@ public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) NetworkServer.AddPlayerForConnection(conn, player); } - /// Called on server when transport raises an exception. NetworkConnection may be null. + // Deprecated 2022-05-12 + [Obsolete("OnServerError(conn, Exception) was changed to OnServerError(conn, TransportError, string)")] public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {} + /// Called on server when transport raises an exception. NetworkConnection may be null. + public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) + { +#pragma warning disable CS0618 + OnServerError(conn, new Exception(reason)); +#pragma warning restore CS0618 + } /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed public virtual void OnServerChangeScene(string newSceneName) {} @@ -1296,33 +1341,23 @@ public virtual void OnClientConnect() } } - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientConnect(NetworkConnection conn) => OnClientConnect(); - /// Called on clients when disconnected from a server. - public virtual void OnClientDisconnect() - { - if (mode == NetworkManagerMode.Offline) - return; - - StopClient(); - } - - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientDisconnect(NetworkConnection conn) => OnClientDisconnect(); + public virtual void OnClientDisconnect() {} - /// Called on client when transport raises an exception. + // Deprecated 2022-05-12 + [Obsolete("OnClientError(Exception) was changed to OnClientError(TransportError, string)")] public virtual void OnClientError(Exception exception) {} + /// Called on client when transport raises an exception. + public virtual void OnClientError(TransportError error, string reason) + { +#pragma warning disable CS0618 + OnClientError(new Exception(reason)); +#pragma warning restore CS0618 + } /// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes. public virtual void OnClientNotReady() {} - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientNotReady(NetworkConnection conn) {} - /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed // customHandling: indicates if scene loading will be handled through overrides public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) {} @@ -1334,20 +1369,16 @@ public virtual void OnClientChangeScene(string newSceneName, SceneOperation scen public virtual void OnClientSceneChanged() { // always become ready. - if (!NetworkClient.ready) NetworkClient.Ready(); + if (NetworkClient.connection.isAuthenticated && !NetworkClient.ready) NetworkClient.Ready(); // Only call AddPlayer for normal scene changes, not additive load/unload - if (clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) + if (NetworkClient.connection.isAuthenticated && clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) { // add player if existing one is null NetworkClient.AddPlayer(); } } - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientSceneChanged(NetworkConnection conn) => OnClientSceneChanged(); - // Since there are multiple versions of StartServer, StartClient and // StartHost, to reliably customize their functionality, users would // need override all the versions. Instead these callbacks are invoked @@ -1370,5 +1401,12 @@ public virtual void OnStopClient() {} /// This is called when a host is stopped. public virtual void OnStopHost() {} + + // keep OnGUI even in builds. useful to debug snap interp. + void OnGUI() + { + if (!timeInterpolationGui) return; + NetworkClient.OnGUI(); + } } } diff --git a/Assets/Mirror/Runtime/NetworkManager.cs.meta b/Assets/Mirror/Core/NetworkManager.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkManager.cs.meta rename to Assets/Mirror/Core/NetworkManager.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs b/Assets/Mirror/Core/NetworkManagerHUD.cs similarity index 93% rename from Assets/Mirror/Runtime/NetworkManagerHUD.cs rename to Assets/Mirror/Core/NetworkManagerHUD.cs index cba968d..0a267fb 100644 --- a/Assets/Mirror/Runtime/NetworkManagerHUD.cs +++ b/Assets/Mirror/Core/NetworkManagerHUD.cs @@ -23,7 +23,7 @@ void Awake() void OnGUI() { - GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 215, 9999)); + GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 250, 9999)); if (!NetworkClient.isConnected && !NetworkServer.active) { StartButtons(); @@ -104,17 +104,17 @@ void StatusLabels() // Client: ... if (NetworkServer.active && NetworkClient.active) { - GUILayout.Label($"Host: running via {Transport.activeTransport}"); + GUILayout.Label($"Host: running via {Transport.active}"); } // server only else if (NetworkServer.active) { - GUILayout.Label($"Server: running via {Transport.activeTransport}"); + GUILayout.Label($"Server: running via {Transport.active}"); } // client only else if (NetworkClient.isConnected) { - GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.activeTransport}"); + GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.active}"); } } @@ -123,10 +123,16 @@ void StopButtons() // stop host if host mode if (NetworkServer.active && NetworkClient.isConnected) { + GUILayout.BeginHorizontal(); if (GUILayout.Button("Stop Host")) { manager.StopHost(); } + if (GUILayout.Button("Stop Client")) + { + manager.StopClient(); + } + GUILayout.EndHorizontal(); } // stop client if client-only else if (NetworkClient.isConnected) diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta b/Assets/Mirror/Core/NetworkManagerHUD.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta rename to Assets/Mirror/Core/NetworkManagerHUD.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs b/Assets/Mirror/Core/NetworkMessage.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkMessage.cs rename to Assets/Mirror/Core/NetworkMessage.cs diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs.meta b/Assets/Mirror/Core/NetworkMessage.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkMessage.cs.meta rename to Assets/Mirror/Core/NetworkMessage.cs.meta diff --git a/Assets/Mirror/Core/NetworkMessages.cs b/Assets/Mirror/Core/NetworkMessages.cs new file mode 100644 index 0000000..c53072e --- /dev/null +++ b/Assets/Mirror/Core/NetworkMessages.cs @@ -0,0 +1,146 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // message packing all in one place, instead of constructing headers in all + // kinds of different places + // + // MsgType (2 bytes) + // Content (ContentSize bytes) + public static class NetworkMessages + { + // size of message id header in bytes + public const int IdSize = sizeof(ushort); + + // max message content size (without header) calculation for convenience + // -> Transport.GetMaxPacketSize is the raw maximum + // -> Every message gets serialized into <> + // -> Every serialized message get put into a batch with a header + public static int MaxContentSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Transport.active.GetMaxPacketSize() + - IdSize + - Batcher.HeaderSize; + } + + // automated message id from type hash. + // platform independent via stable hashcode. + // => convenient so we don't need to track messageIds across projects + // => addons can work with each other without knowing their ids before + // => 2 bytes is enough to avoid collisions. + // registering a messageId twice will log a warning anyway. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort GetId() where T : struct, NetworkMessage => + (ushort)(typeof(T).FullName.GetStableHashCode()); + + // pack message before sending + // -> NetworkWriter passed as arg so that we can use .ToArraySegment + // and do an allocation free send before recycling it. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Pack(T message, NetworkWriter writer) + where T : struct, NetworkMessage + { + writer.WriteUShort(GetId()); + writer.Write(message); + } + + // read only the message id. + // common function in case we ever change the header size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool UnpackId(NetworkReader reader, out ushort messageId) + { + // read message type + try + { + messageId = reader.ReadUShort(); + return true; + } + catch (System.IO.EndOfStreamException) + { + messageId = 0; + return false; + } + } + + // version for handlers with channelId + // inline! only exists for 20-30 messages and they call it all the time. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication) + where T : struct, NetworkMessage + where C : NetworkConnection + => (conn, reader, channelId) => + { + // protect against DOS attacks if attackers try to send invalid + // data packets to crash the server/client. there are a thousand + // ways to cause an exception in data handling: + // - invalid headers + // - invalid message ids + // - invalid data causing exceptions + // - negative ReadBytesAndSize prefixes + // - invalid utf8 strings + // - etc. + // + // let's catch them all and then disconnect that connection to avoid + // further attacks. + T message = default; + // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching + int startPos = reader.Position; + try + { + if (requireAuthentication && !conn.isAuthenticated) + { + // message requires authentication, but the connection was not authenticated + Debug.LogWarning($"Closing connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet"); + conn.Disconnect(); + return; + } + + //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}"); + + // if it is a value type, just use default(T) + // otherwise allocate a new instance + message = reader.Read(); + } + catch (Exception exception) + { + Debug.LogError($"Closed connection: {conn}. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); + conn.Disconnect(); + return; + } + finally + { + int endPos = reader.Position; + // TODO: Figure out the correct channel + NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos); + } + + // user handler exception should not stop the whole server + try + { + // user implemented handler + handler((C)conn, message, channelId); + } + catch (Exception e) + { + Debug.LogError($"Disconnecting connId={conn.connectionId} to prevent exploits from an Exception in MessageHandler: {e.GetType().Name} {e.Message}\n{e.StackTrace}"); + conn.Disconnect(); + } + }; + + // version for handlers without channelId + // TODO obsolete this some day to always use the channelId version. + // all handlers in this version are wrapped with 1 extra action. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication) + where T : struct, NetworkMessage + where C : NetworkConnection + { + // wrap action as channelId version, call original + void Wrapped(C conn, T msg, int _) => handler(conn, msg); + return WrapHandler((Action)Wrapped, requireAuthentication); + } + } +} diff --git a/Assets/Mirror/Runtime/MessagePacking.cs.meta b/Assets/Mirror/Core/NetworkMessages.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/MessagePacking.cs.meta rename to Assets/Mirror/Core/NetworkMessages.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkReader.cs b/Assets/Mirror/Core/NetworkReader.cs similarity index 64% rename from Assets/Mirror/Runtime/NetworkReader.cs rename to Assets/Mirror/Core/NetworkReader.cs index 86eeef4..bec63ce 100644 --- a/Assets/Mirror/Runtime/NetworkReader.cs +++ b/Assets/Mirror/Core/NetworkReader.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; @@ -10,57 +11,70 @@ namespace Mirror // Note: This class is intended to be extremely pedantic, // and throw exceptions whenever stuff is going slightly wrong. // The exceptions will be handled in NetworkServer/NetworkClient. + // + // Note that NetworkWriter can be passed in constructor thanks to implicit + // ArraySegment conversion: + // NetworkReader reader = new NetworkReader(writer); public class NetworkReader { // internal buffer // byte[] pointer would work, but we use ArraySegment to also support // the ArraySegment constructor - ArraySegment buffer; + internal ArraySegment buffer; /// Next position to read from the buffer // 'int' is the best type for .Position. 'short' is too small if we send >32kb which would result in negative .Position // -> converting long to int is fine until 2GB of data (MAX_INT), so we don't have to worry about overflows here public int Position; - /// Total number of bytes to read from buffer - public int Length - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => buffer.Count; - } - /// Remaining bytes that can be read, for convenience. - public int Remaining - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Length - Position; - } + public int Remaining => buffer.Count - Position; - public NetworkReader(byte[] bytes) - { - buffer = new ArraySegment(bytes); - } + /// Total buffer capacity, independent of reader position. + public int Capacity => buffer.Count; + + // cache encoding for ReadString instead of creating it with each time + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + // member(!) to avoid static state. + // + // throwOnInvalidBytes is true. + // if false, it would silently ignore the invalid bytes but continue + // with the valid ones, creating strings like "a�������". + // instead, we want to catch it manually and return String.Empty. + // this is safer. see test: ReadString_InvalidUTF8(). + internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true); public NetworkReader(ArraySegment segment) { buffer = segment; } +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have the implicit byte[] to segment conversion yet + public NetworkReader(byte[] bytes) + { + buffer = new ArraySegment(bytes, 0, bytes.Length); + } +#endif + // sometimes it's useful to point a reader on another buffer instead of // allocating a new reader (e.g. NetworkReaderPool) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetBuffer(byte[] bytes) + public void SetBuffer(ArraySegment segment) { - buffer = new ArraySegment(bytes); + buffer = segment; Position = 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetBuffer(ArraySegment segment) +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have the implicit byte[] to segment conversion yet + public void SetBuffer(byte[] bytes) { - buffer = segment; + buffer = new ArraySegment(bytes, 0, bytes.Length); Position = 0; } +#endif // ReadBlittable from DOTSNET // this is extremely fast, but only works for blittable types. @@ -71,6 +85,26 @@ public void SetBuffer(ArraySegment segment) // Note: // ReadBlittable assumes same endianness for server & client. // All Unity 2018+ platforms are little endian. + // + // This is not safe to expose to random structs. + // * StructLayout.Sequential is the default, which is safe. + // if the struct contains a reference type, it is converted to Auto. + // but since all structs here are unmanaged blittable, it's safe. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential + // * StructLayout.Pack depends on CPU word size. + // this may be different 4 or 8 on some ARM systems, etc. + // this is not safe, and would cause bytes/shorts etc. to be padded. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0 + // * If we force pack all to '1', they would have no padding which is + // great for bandwidth. but on some android systems, CPU can't read + // unaligned memory. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // * The only option would be to force explicit layout with multiples + // of word size. but this requires lots of weaver checking and is + // still questionable (IL2CPP etc.). + // + // Note: inlining ReadBlittable is enough. don't inline ReadInt etc. + // we don't want ReadBlittable to be copied in place everywhere. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe T ReadBlittable() where T : unmanaged @@ -91,10 +125,10 @@ internal unsafe T ReadBlittable() // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices int size = sizeof(T); - // enough data to read? - if (Position + size > buffer.Count) + // ensure remaining + if (Remaining < size) { - throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> out of range: {ToString()}"); + throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> not enough data in buffer to read {size} bytes: {ToString()}"); } // read blittable @@ -132,23 +166,24 @@ internal unsafe T ReadBlittable() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal T? ReadBlittableNullable() where T : unmanaged => - ReadByte() != 0 ? ReadBlittable() : default(T?); + ReadByte() != 0 ? ReadBlittable() : default(T?); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte ReadByte() => ReadBlittable(); /// Read 'count' bytes into the bytes array // NOTE: returns byte[] because all reader functions return something. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte[] ReadBytes(byte[] bytes, int count) { + // user may call ReadBytes(ReadInt()). ensure positive count. + if (count < 0) throw new ArgumentOutOfRangeException("ReadBytes requires count >= 0"); + // check if passed byte array is big enough if (count > bytes.Length) { throw new EndOfStreamException($"ReadBytes can't read {count} + bytes because the passed byte[] only has length {bytes.Length}"); } - // check if within buffer limits - if (Position + count > buffer.Count) + // ensure remaining + if (Remaining < count) { throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}"); } @@ -159,11 +194,13 @@ public byte[] ReadBytes(byte[] bytes, int count) } /// Read 'count' bytes allocation-free as ArraySegment that points to the internal array. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySegment ReadBytesSegment(int count) { - // check if within buffer limits - if (Position + count > buffer.Count) + // user may call ReadBytes(ReadInt()). ensure positive count. + if (count < 0) throw new ArgumentOutOfRangeException("ReadBytesSegment requires count >= 0"); + + // ensure remaining + if (Remaining < count) { throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}"); } @@ -174,10 +211,6 @@ public ArraySegment ReadBytesSegment(int count) return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override string ToString() => - $"NetworkReader pos={Position} len={Length} buffer={BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count)}"; - /// Reads any data type that mirror supports. Uses weaver populated Reader(T).read [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Read() @@ -185,11 +218,15 @@ public T Read() Func readerDelegate = Reader.read; if (readerDelegate == null) { - Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader"); + Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader extension for {typeof(T)}."); return default; } return readerDelegate(this); } + + // print the full buffer with position / capacity. + public override string ToString() => + $"[{buffer.ToHexString()} @ {Position}/{Capacity}]"; } /// Helper class that weaver populates with all reader types. diff --git a/Assets/Mirror/Runtime/NetworkReader.cs.meta b/Assets/Mirror/Core/NetworkReader.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkReader.cs.meta rename to Assets/Mirror/Core/NetworkReader.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs b/Assets/Mirror/Core/NetworkReaderExtensions.cs similarity index 71% rename from Assets/Mirror/Runtime/NetworkReaderExtensions.cs rename to Assets/Mirror/Core/NetworkReaderExtensions.cs index 6137866..8c340f0 100644 --- a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs +++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; -using System.Text; using UnityEngine; namespace Mirror @@ -11,85 +9,52 @@ namespace Mirror // but they do all need to be extensions. public static class NetworkReaderExtensions { - // cache encoding instead of creating it each time - // 1000 readers before: 1MB GC, 30ms - // 1000 readers after: 0.8MB GC, 18ms - static readonly UTF8Encoding encoding = new UTF8Encoding(false, true); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte ReadByte(this NetworkReader reader) => reader.ReadBlittable(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte? ReadByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static sbyte ReadSByte(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static sbyte? ReadSByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); // bool is not blittable. read as ushort. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char ReadChar(this NetworkReader reader) => (char)reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char? ReadCharNullable(this NetworkReader reader) => (char?)reader.ReadBlittableNullable(); // bool is not blittable. read as byte. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ReadBool(this NetworkReader reader) => reader.ReadBlittable() != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool? ReadBoolNullable(this NetworkReader reader) { byte? value = reader.ReadBlittableNullable(); return value.HasValue ? (value.Value != 0) : default(bool?); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static short ReadShort(this NetworkReader reader) => (short)reader.ReadUShort(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static short? ReadShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort ReadUShort(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort? ReadUShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ReadInt(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int? ReadIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint ReadUInt(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint? ReadUIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ReadLong(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long? ReadLongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double ReadDouble(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double? ReadDoubleNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); /// if an invalid utf8 string is sent - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReadString(this NetworkReader reader) { // read number of bytes @@ -110,11 +75,12 @@ public static string ReadString(this NetworkReader reader) ArraySegment data = reader.ReadBytesSegment(realSize); // convert directly from buffer to string via encoding - return encoding.GetString(data.Array, data.Offset, data.Count); + // throws in case of invalid utf8. + // see test: ReadString_InvalidUTF8() + return reader.encoding.GetString(data.Array, data.Offset, data.Count); } /// if count is invalid - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] ReadBytesAndSize(this NetworkReader reader) { // count = 0 means the array was null @@ -124,7 +90,6 @@ public static byte[] ReadBytesAndSize(this NetworkReader reader) return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u))); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] ReadBytes(this NetworkReader reader, int count) { byte[] bytes = new byte[count]; @@ -133,7 +98,6 @@ public static byte[] ReadBytes(this NetworkReader reader, int count) } /// if count is invalid - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader reader) { // count = 0 means the array was null @@ -143,72 +107,61 @@ public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader read return count == 0 ? default : reader.ReadBytesSegment(checked((int)(count - 1u))); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ReadVector2(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2? ReadVector2Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 ReadVector3(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3? ReadVector3Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector4 ReadVector4(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector4? ReadVector4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2Int ReadVector2Int(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2Int? ReadVector2IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3Int ReadVector3Int(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3Int? ReadVector3IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color ReadColor(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color? ReadColorNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color32 ReadColor32(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color32? ReadColor32Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rect ReadRect(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Plane ReadPlane(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Ray ReadRay(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader)=> reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable(); public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid ReadGuid(this NetworkReader reader) => new Guid(reader.ReadBytes(16)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid ReadGuid(this NetworkReader reader) + { +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have Span yet + return new Guid(reader.ReadBytes(16)); +#else + // ReadBlittable(Guid) isn't safe. see ReadBlittable comments. + // Guid is Sequential, but we can't guarantee packing. + if (reader.Remaining >= 16) + { + ReadOnlySpan span = new ReadOnlySpan(reader.buffer.Array, reader.buffer.Offset + reader.Position, 16); + reader.Position += 16; + return new Guid(span); + } + throw new EndOfStreamException($"ReadGuid out of range: {reader}"); +#endif + } public static Guid? ReadGuidNullable(this NetworkReader reader) => reader.ReadBool() ? ReadGuid(reader) : default(Guid?); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader) { uint netId = reader.ReadUInt(); @@ -222,7 +175,6 @@ public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader) return Utils.GetSpawnedInServerOrClient(netId); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader) { // read netId first. @@ -247,18 +199,16 @@ public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader) NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId); return identity != null - ? identity.NetworkBehaviours[componentIndex] - : null; + ? identity.NetworkBehaviours[componentIndex] + : null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T ReadNetworkBehaviour(this NetworkReader reader) where T : NetworkBehaviour { return reader.ReadNetworkBehaviour() as T; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader) + public static NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader) { uint netId = reader.ReadUInt(); byte componentIndex = default; @@ -269,10 +219,9 @@ public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncV componentIndex = reader.ReadByte(); } - return new NetworkBehaviour.NetworkBehaviourSyncVar(netId, componentIndex); + return new NetworkBehaviourSyncVar(netId, componentIndex); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Transform ReadTransform(this NetworkReader reader) { // Don't use null propagation here as it could lead to MissingReferenceException @@ -280,7 +229,6 @@ public static Transform ReadTransform(this NetworkReader reader) return networkIdentity != null ? networkIdentity.transform : null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static GameObject ReadGameObject(this NetworkReader reader) { // Don't use null propagation here as it could lead to MissingReferenceException @@ -288,7 +236,10 @@ public static GameObject ReadGameObject(this NetworkReader reader) return networkIdentity != null ? networkIdentity.gameObject : null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // while SyncList is recommended for NetworkBehaviours, + // structs may have .List members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Readers/GenerateReader() handles this manually. public static List ReadList(this NetworkReader reader) { int length = reader.ReadInt(); @@ -302,7 +253,26 @@ public static List ReadList(this NetworkReader reader) return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // while SyncSet is recommended for NetworkBehaviours, + // structs may have .Set members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Readers/GenerateReader() handles this manually. + // TODO writer not found. need to adjust weaver first. see tests. + /* + public static HashSet ReadHashSet(this NetworkReader reader) + { + int length = reader.ReadInt(); + if (length < 0) + return null; + HashSet result = new HashSet(); + for (int i = 0; i < length; i++) + { + result.Add(reader.Read()); + } + return result; + } + */ + public static T[] ReadArray(this NetworkReader reader) { int length = reader.ReadInt(); @@ -316,7 +286,7 @@ public static T[] ReadArray(this NetworkReader reader) // this assumes that a reader for T reads at least 1 bytes // we can't know the exact size of T because it could have a user created reader // NOTE: don't add to length as it could overflow if value is int.max - if (length > reader.Length - reader.Position) + if (length > reader.Remaining) { throw new EndOfStreamException($"Received array that is too large: {length}"); } @@ -329,26 +299,42 @@ public static T[] ReadArray(this NetworkReader reader) return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Uri ReadUri(this NetworkReader reader) { string uriString = reader.ReadString(); return (string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Texture2D ReadTexture2D(this NetworkReader reader) { - Texture2D texture2D = new Texture2D(32, 32); - texture2D.SetPixels32(reader.Read()); + // TODO allocation protection when sending textures to server. + // currently can allocate 32k x 32k x 4 byte = 3.8 GB + + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + short width = reader.ReadShort(); + if (width == -1) return null; + + // read height + short height = reader.ReadShort(); + Texture2D texture2D = new Texture2D(width, height); + + // read pixel content + Color32[] pixels = reader.ReadArray(); + texture2D.SetPixels32(pixels); texture2D.Apply(); return texture2D; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Sprite ReadSprite(this NetworkReader reader) { - return Sprite.Create(reader.ReadTexture2D(), reader.ReadRect(), reader.ReadVector2()); + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + Texture2D texture = reader.ReadTexture2D(); + if (texture == null) return null; + + // otherwise create a valid sprite + return Sprite.Create(texture, reader.ReadRect(), reader.ReadVector2()); } } } diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta b/Assets/Mirror/Core/NetworkReaderExtensions.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta rename to Assets/Mirror/Core/NetworkReaderExtensions.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkReaderPool.cs b/Assets/Mirror/Core/NetworkReaderPool.cs similarity index 76% rename from Assets/Mirror/Runtime/NetworkReaderPool.cs rename to Assets/Mirror/Core/NetworkReaderPool.cs index ebbfac5..15708b7 100644 --- a/Assets/Mirror/Runtime/NetworkReaderPool.cs +++ b/Assets/Mirror/Core/NetworkReaderPool.cs @@ -17,10 +17,6 @@ public static class NetworkReaderPool 1000 ); - // DEPRECATED 2022-03-10 - [Obsolete("GetReader() was renamed to Get()")] - public static NetworkReaderPooled GetReader(byte[] bytes) => Get(bytes); - /// Get the next reader in the pool. If pool is empty, creates a new Reader [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkReaderPooled Get(byte[] bytes) @@ -31,10 +27,6 @@ public static NetworkReaderPooled Get(byte[] bytes) return reader; } - // DEPRECATED 2022-03-10 - [Obsolete("GetReader() was renamed to Get()")] - public static NetworkReaderPooled GetReader(ArraySegment segment) => Get(segment); - /// Get the next reader in the pool. If pool is empty, creates a new Reader [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkReaderPooled Get(ArraySegment segment) @@ -45,10 +37,6 @@ public static NetworkReaderPooled Get(ArraySegment segment) return reader; } - // DEPRECATED 2022-03-10 - [Obsolete("Recycle() was renamed to Return()")] - public static void Recycle(NetworkReaderPooled reader) => Return(reader); - /// Returns a reader to the pool. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Return(NetworkReaderPooled reader) diff --git a/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta b/Assets/Mirror/Core/NetworkReaderPool.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkReaderPool.cs.meta rename to Assets/Mirror/Core/NetworkReaderPool.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkReaderPooled.cs b/Assets/Mirror/Core/NetworkReaderPooled.cs similarity index 64% rename from Assets/Mirror/Runtime/NetworkReaderPooled.cs rename to Assets/Mirror/Core/NetworkReaderPooled.cs index fcfa792..5348904 100644 --- a/Assets/Mirror/Runtime/NetworkReaderPooled.cs +++ b/Assets/Mirror/Core/NetworkReaderPooled.cs @@ -4,13 +4,6 @@ namespace Mirror { - [Obsolete("PooledNetworkReader was renamed to NetworkReaderPooled. It's cleaner & slightly easier to use.")] - public sealed class PooledNetworkReader : NetworkReaderPooled - { - internal PooledNetworkReader(byte[] bytes) : base(bytes) {} - internal PooledNetworkReader(ArraySegment segment) : base(segment) {} - } - /// Pooled NetworkReader, automatically returned to pool when using 'using' // TODO make sealed again after removing obsolete NetworkReaderPooled! public class NetworkReaderPooled : NetworkReader, IDisposable diff --git a/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta b/Assets/Mirror/Core/NetworkReaderPooled.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta rename to Assets/Mirror/Core/NetworkReaderPooled.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs similarity index 79% rename from Assets/Mirror/Runtime/NetworkServer.cs rename to Assets/Mirror/Core/NetworkServer.cs index 45d00c4..8784fa2 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -7,22 +7,39 @@ namespace Mirror { /// NetworkServer handles remote connections and has a local connection for a local client. - public static class NetworkServer + public static partial class NetworkServer { static bool initialized; public static int maxConnections; - /// Connection to host mode client (if any) - public static NetworkConnectionToClient localConnection { get; private set; } + /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + // overwritten by NetworkManager (if any) + public static int tickRate = 30; + + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms + + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + public static int sendRate => tickRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; - /// True is a local client is currently active on the server - public static bool localClientActive => localConnection != null; + /// Connection to host mode client (if any) + public static LocalConnectionToClient localConnection { get; private set; } /// Dictionary of all server connections, with connectionId as key public static Dictionary connections = new Dictionary(); - /// Message Handlers dictionary, with mesageId as key + /// Message Handlers dictionary, with messageId as key internal static Dictionary handlers = new Dictionary(); @@ -35,9 +52,17 @@ public static class NetworkServer // see also: https://github.com/vis2k/Mirror/pull/2595 public static bool dontListen; - /// active checks if the server has been started + // Deprecated 2022-12-12 + [Obsolete("NetworkServer.localClientActive was renamed to .activeHost to be more obvious")] + public static bool localClientActive => activeHost; + + /// active checks if the server has been started either has standalone or as host server. public static bool active { get; internal set; } + /// active checks if the server has been started in host mode. + // naming consistent with NetworkClient.activeHost. + public static bool activeHost => localConnection != null; + // scene loading public static bool isLoadingScene; @@ -52,7 +77,41 @@ public static class NetworkServer // => public so that custom NetworkManagers can hook into it public static Action OnConnectedEvent; public static Action OnDisconnectedEvent; - public static Action OnErrorEvent; + public static Action OnErrorEvent; + + // keep track of actual achieved tick rate. + // might become lower under heavy load. + // very useful for profiling etc. + // measured over 1s each, same as frame rate. no EMA here. + public static int actualTickRate; + static double actualTickRateStart; // start time when counting + static int actualTickRateCounter; // current counter since start + + // profiling + // includes transport update time, because transport calls handlers etc. + // averaged over 1s by passing 'tickRate' to constructor. + public static TimeSample earlyUpdateDuration; + public static TimeSample lateUpdateDuration; + + // capture full Unity update time from before Early- to after LateUpdate + public static TimeSample fullUpdateDuration; + + /// Starts server and listens to incoming connections with max connections limit. + public static void Listen(int maxConns) + { + Initialize(); + maxConnections = maxConns; + + // only start server if we want to listen + if (!dontListen) + { + Transport.active.ServerStart(); + //Debug.Log("Server started listening"); + } + + active = true; + RegisterMessageHandlers(); + } // initialization / shutdown /////////////////////////////////////////// static void Initialize() @@ -72,66 +131,95 @@ static void Initialize() // reset NetworkTime NetworkTime.ResetStatics(); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.activeTransport' first"); + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); AddTransportHandlers(); initialized = true; + + // profiling + earlyUpdateDuration = new TimeSample(sendRate); + lateUpdateDuration = new TimeSample(sendRate); + fullUpdateDuration = new TimeSample(sendRate); } static void AddTransportHandlers() { // += so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnServerConnected += OnTransportConnected; - Transport.activeTransport.OnServerDataReceived += OnTransportData; - Transport.activeTransport.OnServerDisconnected += OnTransportDisconnected; - Transport.activeTransport.OnServerError += OnError; - } - - static void RemoveTransportHandlers() - { - // -= so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnServerConnected -= OnTransportConnected; - Transport.activeTransport.OnServerDataReceived -= OnTransportData; - Transport.activeTransport.OnServerDisconnected -= OnTransportDisconnected; - Transport.activeTransport.OnServerError -= OnError; + Transport.active.OnServerConnected += OnTransportConnected; + Transport.active.OnServerDataReceived += OnTransportData; + Transport.active.OnServerDisconnected += OnTransportDisconnected; + Transport.active.OnServerError += OnTransportError; } - // calls OnStartClient for all SERVER objects in host mode once. - // client doesn't get spawn messages for those, so need to call manually. - public static void ActivateHostScene() + /// Shuts down the server and disconnects all clients + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Shutdown() { - foreach (NetworkIdentity identity in spawned.Values) + if (initialized) { - if (!identity.isClient) - { - // Debug.Log($"ActivateHostScene {identity.netId} {identity}"); - identity.OnStartClient(); - } - } - } + DisconnectAll(); - internal static void RegisterMessageHandlers() - { - RegisterHandler(OnClientReadyMessage); - RegisterHandler(OnCommandMessage); - RegisterHandler(NetworkTime.OnServerPing, false); - } + // stop the server. + // we do NOT call Transport.Shutdown, because someone only + // called NetworkServer.Shutdown. we can't assume that the + // client is supposed to be shut down too! + // + // NOTE: stop no matter what, even if 'dontListen': + // someone might enabled dontListen at runtime. + // but we still need to stop the server. + // fixes https://github.com/vis2k/Mirror/issues/2536 + Transport.active.ServerStop(); - /// Starts server and listens to incoming connections with max connections limit. - public static void Listen(int maxConns) - { - Initialize(); - maxConnections = maxConns; + // transport handlers are hooked into when initializing. + // so only remove them when shutting down. + RemoveTransportHandlers(); - // only start server if we want to listen - if (!dontListen) - { - Transport.activeTransport.ServerStart(); - //Debug.Log("Server started listening"); + initialized = false; } - active = true; - RegisterMessageHandlers(); + // Reset all statics here.... + dontListen = false; + isLoadingScene = false; + lastSendTime = 0; + actualTickRate = 0; + + localConnection = null; + + connections.Clear(); + connectionsCopy.Clear(); + handlers.Clear(); + newObservers.Clear(); + + // destroy all spawned objects, _then_ set inactive. + // make sure .active is still true before calling this. + // otherwise modifying SyncLists in OnStopServer would throw + // because .IsWritable() check checks if NetworkServer.active. + // https://github.com/MirrorNetworking/Mirror/issues/3344 + CleanupSpawned(); + active = false; + + // sets nextNetworkId to 1 + // sets clientAuthorityCallback to null + // sets previousLocalPlayer to null + NetworkIdentity.ResetStatics(); + + // clear events. someone might have hooked into them before, but + // we don't want to use those hooks after Shutdown anymore. + OnConnectedEvent = null; + OnDisconnectedEvent = null; + OnErrorEvent = null; + + if (aoi != null) aoi.Reset(); + } + + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) + Transport.active.OnServerConnected -= OnTransportConnected; + Transport.active.OnServerDataReceived -= OnTransportData; + Transport.active.OnServerDisconnected -= OnTransportDisconnected; + Transport.active.OnServerError -= OnTransportError; } // Note: NetworkClient.DestroyAllClientObjects does the same on client. @@ -165,60 +253,119 @@ static void CleanupSpawned() spawned.Clear(); } - /// Shuts down the server and disconnects all clients - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - public static void Shutdown() + internal static void RegisterMessageHandlers() { - if (initialized) - { - DisconnectAll(); + RegisterHandler(OnClientReadyMessage); + RegisterHandler(OnCommandMessage); + RegisterHandler(NetworkTime.OnServerPing, false); + RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnTimeSnapshotMessage, true); + } - // stop the server. - // we do NOT call Transport.Shutdown, because someone only - // called NetworkServer.Shutdown. we can't assume that the - // client is supposed to be shut down too! - // - // NOTE: stop no matter what, even if 'dontListen': - // someone might enabled dontListen at runtime. - // but we still need to stop the server. - // fixes https://github.com/vis2k/Mirror/issues/2536 - Transport.activeTransport.ServerStop(); + // remote calls //////////////////////////////////////////////////////// + // Handle command from specific player, this could be one of multiple + // players on a single client + // default ready handler. + static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) + { + // Debug.Log($"Default handler for ready message from {conn}"); + SetClientReady(conn); + } - // transport handlers are hooked into when initializing. - // so only remove them when shutting down. - RemoveTransportHandlers(); + static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) + { + if (!conn.isReady) + { + // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. + // Ignore commands that may have been in flight before client received NotReadyMessage message. + // Unreliable messages may be out of order, so don't spam warnings for those. + if (channelId == Channels.Reliable) + Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } - initialized = false; + if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + // over reliable channel, commands should always come after spawn. + // over unreliable, they might come in before the object was spawned. + // for example, NetworkTransform. + // let's not spam the console for unreliable out of order messages. + if (channelId == Channels.Reliable) + Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]"); + return; } - // Reset all statics here.... - dontListen = false; - active = false; - isLoadingScene = false; + // Commands can be for player objects, OR other objects with client-authority + // -> so if this connection's controller has a different netId then + // only allow the command if clientAuthorityOwner + bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); + if (requiresAuthority && identity.connectionToClient != conn) + { + Debug.LogWarning($"Command for object without authority [netId={msg.netId}]"); + return; + } - localConnection = null; + // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); - connections.Clear(); - connectionsCopy.Clear(); - handlers.Clear(); - newObservers.Clear(); + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) + identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); + } - // this calls spawned.Clear() - CleanupSpawned(); + // client to server broadcast ////////////////////////////////////////// + // for client's owned ClientToServer components. + static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) + { + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. - // sets nextNetworkId to 1 - // sets clientAuthorityCallback to null - // sets previousLocalPlayer to null - NetworkIdentity.ResetStatics(); + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + if (!identity.DeserializeServer(reader)) + { + Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + } + } + // an attacker may attempt to modify another connection's entity + else + { + Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection."); + connection.Disconnect(); + } + } + // no warning. don't spam server logs. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } - // clear events. someone might have hooked into them before, but - // we don't want to use those hooks after Shutdown anymore. - OnConnectedEvent = null; - OnDisconnectedEvent = null; - OnErrorEvent = null; + // client sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. - if (aoi != null) aoi.Reset(); + // TODO validation? + // maybe we shouldn't allow timeline to deviate more than a certain %. + // for now, this is only used for client authority movement. + +#if !UNITY_2020_3_OR_NEWER + // Unity 2019 doesn't have Time.timeAsDouble yet + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); +#else + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); +#endif } // connections ///////////////////////////////////////////////////////// @@ -264,11 +411,6 @@ internal static void RemoveLocalConnection() RemoveConnection(0); } - /// True if we have no external connections (host is allowed) - // DEPRECATED 2022-02-05 - [Obsolete("Use !HasExternalConnections() instead of NoExternalConnections() to avoid double negatives.")] - public static bool NoExternalConnections() => !HasExternalConnections(); - /// True if we have external connections (that are not host) public static bool HasExternalConnections() { @@ -300,7 +442,7 @@ public static void SendToAll(T message, int channelId = Channels.Reliable, bo using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); // filter and then send to all internet connections at once @@ -340,16 +482,16 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI where T : struct, NetworkMessage { // Debug.Log($"Server.SendToObservers {typeof(T)}"); - if (identity == null || identity.observers == null || identity.observers.Count == 0) + if (identity == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message into byte[] once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); - foreach (NetworkConnection conn in identity.observers.Values) + foreach (NetworkConnectionToClient conn in identity.observers.Values) { conn.Send(segment, channelId); } @@ -359,22 +501,22 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI } /// Send a message to only clients which are ready with option to include the owner of the object identity - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + // TODO obsolete this later. it's not used anymore public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) where T : struct, NetworkMessage { // Debug.Log($"Server.SendToReady {typeof(T)}"); - if (identity == null || identity.observers == null || identity.observers.Count == 0) + if (identity == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); int count = 0; - foreach (NetworkConnection conn in identity.observers.Values) + foreach (NetworkConnectionToClient conn in identity.observers.Values) { bool isOwner = conn == identity.connectionToClient; if ((!isOwner || includeOwner) && conn.isReady) @@ -388,26 +530,14 @@ public static void SendToReadyObservers(NetworkIdentity identity, T message, } } - // Deprecated 2021-09-19 - [Obsolete("SendToReady(identity, message, ...) was renamed to SendToReadyObservers because that's what it does.")] - public static void SendToReady(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) - where T : struct, NetworkMessage => - SendToReadyObservers(identity, message, includeOwner, channelId); - /// Send a message to only clients which are ready including the owner of the NetworkIdentity - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + // TODO obsolete this later. it's not used anymore public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) where T : struct, NetworkMessage { SendToReadyObservers(identity, message, true, channelId); } - // Deprecated 2021-09-19 - [Obsolete("SendToReady(identity, message, ...) was renamed to SendToReadyObservers because that's what it does.")] - public static void SendToReady(NetworkIdentity identity, T message, int channelId) - where T : struct, NetworkMessage => - SendToReadyObservers(identity, message, channelId); - // transport events //////////////////////////////////////////////////// // called by transport static void OnTransportConnected(int connectionId) @@ -420,14 +550,14 @@ static void OnTransportConnected(int connectionId) if (connectionId == 0) { Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); - Transport.activeTransport.ServerDisconnect(connectionId); + Transport.active.ServerDisconnect(connectionId); return; } // connectionId not in use yet? if (connections.ContainsKey(connectionId)) { - Transport.activeTransport.ServerDisconnect(connectionId); + Transport.active.ServerDisconnect(connectionId); // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); return; } @@ -446,7 +576,7 @@ static void OnTransportConnected(int connectionId) else { // kick - Transport.activeTransport.ServerDisconnect(connectionId); + Transport.active.ServerDisconnect(connectionId); // Debug.Log($"Server full, kicked client {connectionId}"); } } @@ -462,7 +592,7 @@ internal static void OnConnected(NetworkConnectionToClient conn) static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) { - if (MessagePacking.Unpack(reader, out ushort msgType)) + if (NetworkMessages.UnpackId(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) @@ -524,7 +654,7 @@ internal static void OnTransportData(int connectionId, ArraySegment data, connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) { // enough to read at least header size? - if (reader.Remaining >= MessagePacking.HeaderSize) + if (reader.Remaining >= NetworkMessages.IdSize) { // make remoteTimeStamp available to the user connection.remoteTimeStamp = remoteTimestamp; @@ -607,12 +737,32 @@ internal static void OnTransportDisconnected(int connectionId) } } - static void OnError(int connectionId, Exception exception) + // transport errors are forwarded to high level + static void OnTransportError(int connectionId, TransportError error, string reason) { - Debug.LogException(exception); + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Server Transport Error for connId={connectionId}: {error}: {reason}. This is fine."); // try get connection. passes null otherwise. connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); - OnErrorEvent?.Invoke(conn, exception); + OnErrorEvent?.Invoke(conn, error, reason); + } + + /// Destroys all of the connection's owned objects on the server. + // This is used when a client disconnects, to remove the players for + // that client. This also destroys non-player objects that have client + // authority set for this connection. + public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) + { + // destroy all objects owned by this connection, including the player object + conn.DestroyOwnedObjects(); + // remove connection from all of its observing entities observers + // fixes https://github.com/vis2k/Mirror/issues/2737 + // -> cleaning those up in NetworkConnection.Disconnect is NOT enough + // because voluntary disconnects from the other end don't call + // NetworkConnection.Disconnect() + conn.RemoveFromObservingsObservers(); + conn.identity = null; } // message handlers //////////////////////////////////////////////////// @@ -622,12 +772,12 @@ static void OnError(int connectionId, Exception exception) public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Register a handler for message type T. Most should require authentication. @@ -635,34 +785,34 @@ public static void RegisterHandler(Action handl public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); + ushort msgType = NetworkMessages.GetId(); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Unregister a handler for a message type T. public static void UnregisterHandler() where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); handlers.Remove(msgType); } @@ -671,8 +821,7 @@ public static void UnregisterHandler() internal static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) { - identity = go.GetComponent(); - if (identity == null) + if (!go.TryGetComponent(out identity)) { Debug.LogError($"GameObject {go.name} doesn't have NetworkIdentity."); return false; @@ -693,7 +842,7 @@ public static void DisconnectAll() // we are iterating here. // see also: https://github.com/vis2k/Mirror/issues/2357 // this whole process should be simplified some day. - // until then, let's copy .Values to avoid InvalidOperatinException. + // until then, let's copy .Values to avoid InvalidOperationException. // note that this is only called when stopping the server, so the // copy is no performance problem. foreach (NetworkConnectionToClient conn in connections.Values.ToList()) @@ -716,10 +865,30 @@ public static void DisconnectAll() // cleanup connections.Clear(); localConnection = null; - active = false; + // this used to set active=false. + // however, then Shutdown can't properly destroy objects: + // https://github.com/MirrorNetworking/Mirror/issues/3344 + // "DisconnectAll" should only disconnect all, not set inactive. + // active = false; } // add/remove/replace player /////////////////////////////////////////// + /// Called by server after AddPlayer message to add the player for the connection. + // When a player is added for a connection, the client for that + // connection is made ready automatically. The player object is + // automatically spawned, so you do not need to call NetworkServer.Spawn + // for that object. This function is used for "adding" a player, not for + // "replacing" the player on a connection. If there is already a player + // on this playerControllerId for this connection, this will fail. + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return AddPlayerForConnection(conn, player); + } + /// Called by server after AddPlayer message to add the player for the connection. // When a player is added for a connection, the client for that // connection is made ready automatically. The player object is @@ -729,8 +898,7 @@ public static void DisconnectAll() // on this playerControllerId for this connection, this will fail. public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player) { - NetworkIdentity identity = player.GetComponent(); - if (identity == null) + if (!player.TryGetComponent(out NetworkIdentity identity)) { Debug.LogWarning($"AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); return false; @@ -753,7 +921,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.hasAuthority = true; + identity.isOwned = true; NetworkClient.InternalAddPlayer(identity); } @@ -766,29 +934,12 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb return true; } - /// Called by server after AddPlayer message to add the player for the connection. - // When a player is added for a connection, the client for that - // connection is made ready automatically. The player object is - // automatically spawned, so you do not need to call NetworkServer.Spawn - // for that object. This function is used for "adding" a player, not for - // "replacing" the player on a connection. If there is already a player - // on this playerControllerId for this connection, this will fail. - public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId) - { - if (GetNetworkIdentity(player, out NetworkIdentity identity)) - { - identity.assetId = assetId; - } - return AddPlayerForConnection(conn, player); - } - /// Replaces connection's player object. The old object is not destroyed. // This does NOT change the ready state of the connection, so it can // safely be used while changing scenes. public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, bool keepAuthority = false) { - NetworkIdentity identity = player.GetComponent(); - if (identity == null) + if (!player.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"ReplacePlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); return false; @@ -813,7 +964,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.hasAuthority = true; + identity.isOwned = true; NetworkClient.InternalAddPlayer(identity); } @@ -846,7 +997,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga /// Replaces connection's player object. The old object is not destroyed. // This does NOT change the ready state of the connection, so it can // safely be used while changing scenes. - public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId, bool keepAuthority = false) + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) { if (GetNetworkIdentity(player, out NetworkIdentity identity)) { @@ -855,6 +1006,22 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga return ReplacePlayerForConnection(conn, player, keepAuthority); } + /// Removes the player object from the connection + // destroyServerObject: Indicates whether the server object should be destroyed + public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject) + { + if (conn.identity != null) + { + if (destroyServerObject) + Destroy(conn.identity.gameObject); + else + UnSpawn(conn.identity.gameObject); + + conn.identity = null; + } + //else Debug.Log($"Connection {conn} has no identity"); + } + // ready /////////////////////////////////////////////////////////////// /// Flags client connection as ready (=joined world). // When a client has signaled that it is ready, this method tells the @@ -875,6 +1042,73 @@ public static void SetClientReady(NetworkConnectionToClient conn) SpawnObserversForConnection(conn); } + static void SpawnObserversForConnection(NetworkConnectionToClient conn) + { + //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); + + if (!conn.isReady) + { + // client needs to finish initializing before we can spawn objects + // otherwise it would not find them. + return; + } + + // let connection know that we are about to start spawning... + conn.Send(new ObjectSpawnStartedMessage()); + + // add connection to each nearby NetworkIdentity's observers, which + // internally sends a spawn message for each one to the connection. + foreach (NetworkIdentity identity in spawned.Values) + { + // try with far away ones in ummorpg! + if (identity.gameObject.activeSelf) //TODO this is different + { + //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); + + // we need to support three cases: + // - legacy system (identity has .visibility) + // - new system (networkserver has .aoi) + // - default case: no .visibility and no .aoi means add all + // connections by default) + // + // ForceHidden/ForceShown overwrite all systems so check it + // first! + + // ForceShown: add no matter what + if (identity.visible == Visibility.ForceShown) + { + identity.AddObserver(conn); + } + // ForceHidden: don't show no matter what + else if (identity.visible == Visibility.ForceHidden) + { + // do nothing + } + // default: legacy system / new system / no system support + else if (identity.visible == Visibility.Default) + { + // aoi system + if (aoi != null) + { + // call OnCheckObserver + if (aoi.OnCheckObserver(identity, conn)) + identity.AddObserver(conn); + } + // no system: add all observers by default + else + { + identity.AddObserver(conn); + } + } + } + } + + // let connection know that we finished spawning, so it can call + // OnStartClient on each one (only after all were spawned, which + // is how Unity's Start() function works too) + conn.Send(new ObjectSpawnFinishedMessage()); + } + /// Marks the client of the connection to be not-ready. // Clients that are not ready do not receive spawned objects or state // synchronization updates. They client can be made ready again by @@ -898,13 +1132,6 @@ public static void SetAllClientsNotReady() } } - // default ready handler. - static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) - { - // Debug.Log($"Default handler for ready message from {conn}"); - SetClientReady(conn); - } - // show / hide for connection ////////////////////////////////////////// internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn) { @@ -921,89 +1148,7 @@ internal static void HideForConnection(NetworkIdentity identity, NetworkConnecti conn.Send(msg); } - /// Removes the player object from the connection - // destroyServerObject: Indicates whether the server object should be destroyed - public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject) - { - if (conn.identity != null) - { - if (destroyServerObject) - Destroy(conn.identity.gameObject); - else - UnSpawn(conn.identity.gameObject); - - conn.identity = null; - } - //else Debug.Log($"Connection {conn} has no identity"); - } - - // remote calls //////////////////////////////////////////////////////// - // Handle command from specific player, this could be one of multiple - // players on a single client - static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) - { - if (!conn.isReady) - { - // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. - // Ignore commands that may have been in flight before client received NotReadyMessage message. - // Unreliable messages may be out of order, so don't spam warnings for those. - if (channelId == Channels.Reliable) - Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady."); - return; - } - - if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) - { - // over reliable channel, commands should always come after spawn. - // over unreliable, they might come in before the object was spawned. - // for example, NetworkTransform. - // let's not spam the console for unreliable out of order messages. - if (channelId == Channels.Reliable) - Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]"); - return; - } - - // Commands can be for player objects, OR other objects with client-authority - // -> so if this connection's controller has a different netId then - // only allow the command if clientAuthorityOwner - bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); - if (requiresAuthority && identity.connectionToClient != conn) - { - Debug.LogWarning($"Command for object without authority [netId={msg.netId}]"); - return; - } - - // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); - - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) - identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn as NetworkConnectionToClient); - } - // spawning //////////////////////////////////////////////////////////// - static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) - { - // Only call OnSerializeAllSafely if there are NetworkBehaviours - if (identity.NetworkBehaviours.Length == 0) - { - return default; - } - - // serialize all components with initialState = true - // (can be null if has none) - identity.OnSerializeAllSafely(true, ownerWriter, observersWriter); - - // convert to ArraySegment to avoid reader allocations - // if nothing was written, .ToArraySegment returns an empty segment. - ArraySegment ownerSegment = ownerWriter.ToArraySegment(); - ArraySegment observersSegment = observersWriter.ToArraySegment(); - - // use owner segment if 'conn' owns this identity, otherwise - // use observers segment - ArraySegment payload = isOwner ? ownerSegment : observersSegment; - - return payload; - } - internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) { if (identity.serverOnly) return; @@ -1032,6 +1177,30 @@ internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectio } } + static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) + { + // Only call SerializeAll if there are NetworkBehaviours + if (identity.NetworkBehaviours.Length == 0) + { + return default; + } + + // serialize all components with initialState = true + // (can be null if has none) + identity.SerializeServer(true, ownerWriter, observersWriter); + + // convert to ArraySegment to avoid reader allocations + // if nothing was written, .ToArraySegment returns an empty segment. + ArraySegment ownerSegment = ownerWriter.ToArraySegment(); + ArraySegment observersSegment = observersWriter.ToArraySegment(); + + // use owner segment if 'conn' owns this identity, otherwise + // use observers segment + ArraySegment payload = isOwner ? ownerSegment : observersSegment; + + return payload; + } + internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkConnectionToClient conn) { // Don't send if identity isn't spawned or only exists on server @@ -1051,77 +1220,75 @@ internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkCon }); } - static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) - { - // verify if we can spawn this - if (Utils.IsPrefab(obj)) - { - Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); - return; - } + // check NetworkIdentity parent before spawning it. + // - without parent, they are spawned + // - with parent, only if the parent is active in hierarchy + // + // note that active parents may have inactive parents of their own. + // we need to check .activeInHierarchy. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + static bool ValidParent(NetworkIdentity identity) => + identity.transform.parent == null || + identity.transform.parent.gameObject.activeInHierarchy; + /// Spawns NetworkIdentities in the scene on the server. + // NetworkIdentity objects in a scene are disabled by default. Calling + // SpawnObjects() causes these scene objects to be enabled and spawned. + // It is like calling NetworkServer.Spawn() for each of them. + public static bool SpawnObjects() + { + // only if server active if (!active) - { - Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server."); - return; - } + return false; - NetworkIdentity identity = obj.GetComponent(); - if (identity == null) - { - Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); - return; - } + // find all NetworkIdentities in the scene. + // all of them are disabled because of NetworkScenePostProcess. + NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); - if (identity.SpawnedFromInstantiate) + // first pass: activate all scene objects + foreach (NetworkIdentity identity in identities) { - // Using Instantiate on SceneObject is not allowed, so stop spawning here - // NetworkIdentity.Awake already logs error, no need to log a second error here - return; + // only spawn scene objects which haven't been spawned yet. + // SpawnObjects may be called multiple times for additive scenes. + // https://github.com/MirrorNetworking/Mirror/issues/3318 + // + // note that we even activate objects under inactive parents. + // while they are not spawned, they do need to be activated + // in order to be spawned later. so here, we don't check parents. + // https://github.com/MirrorNetworking/Mirror/issues/3330 + if (Utils.IsSceneObject(identity) && identity.netId == 0) + { + // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); + identity.gameObject.SetActive(true); + } } - identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; - - // special case to make sure hasAuthority is set - // on start server in host mode - if (ownerConnection is LocalConnectionToClient) - identity.hasAuthority = true; - - identity.OnStartServer(); - - // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); - - if (aoi) + // second pass: spawn all scene objects + foreach (NetworkIdentity identity in identities) { - // This calls user code which might throw exceptions - // We don't want this to leave us in bad state - try + // scene objects may be children of inactive parents. + // users would put them under disabled parents to 'deactivate' them. + // those should not be used by Mirror at all. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + if (Utils.IsSceneObject(identity) && identity.netId == 0 && ValidParent(identity)) { - aoi.OnSpawned(identity); - } - catch (Exception e) - { - Debug.LogException(e); + // pass connection so that authority is not lost when server loads a scene + // https://github.com/vis2k/Mirror/pull/2987 + Spawn(identity.gameObject, identity.connectionToClient); } } - RebuildObservers(identity, true); - } - - /// Spawn the given game object on all clients which are ready. - // This will cause a new object to be instantiated from the registered - // prefab, or from a custom spawn function. - public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) - { - SpawnObject(obj, ownerConnection); + return true; } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) { - NetworkIdentity identity = ownerPlayer.GetComponent(); - if (identity == null) + if (!ownerPlayer.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError("Player object has no NetworkIdentity"); return; @@ -1136,155 +1303,119 @@ public static void Spawn(GameObject obj, GameObject ownerPlayer) Spawn(obj, identity.connectionToClient); } - /// Spawns an object and also assigns Client Authority to the specified client. - // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) + static void Respawn(NetworkIdentity identity) { - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + if (identity.netId == 0) { - identity.assetId = assetId; + // If the object has not been spawned, then do a full spawn and update observers + Spawn(identity.gameObject, identity.connectionToClient); + } + else + { + // otherwise just replace his data + SendSpawnMessage(identity, identity.connectionToClient); } - SpawnObject(obj, ownerConnection); } - internal static bool ValidateSceneObject(NetworkIdentity identity) + /// Spawn the given game object on all clients which are ready. + // This will cause a new object to be instantiated from the registered + // prefab, or from a custom spawn function. + public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) { - if (identity.gameObject.hideFlags == HideFlags.NotEditable || - identity.gameObject.hideFlags == HideFlags.HideAndDontSave) - return false; - -#if UNITY_EDITOR - if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) - return false; -#endif - - // If not a scene object - return identity.sceneId != 0; + SpawnObject(obj, ownerConnection); } - /// Spawns NetworkIdentities in the scene on the server. - // NetworkIdentity objects in a scene are disabled by default. Calling - // SpawnObjects() causes these scene objects to be enabled and spawned. - // It is like calling NetworkServer.Spawn() for each of them. - public static bool SpawnObjects() + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerConnection = null) { - // only if server active - if (!active) - return false; - - NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); - - // first pass: activate all scene objects - foreach (NetworkIdentity identity in identities) + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) { - if (ValidateSceneObject(identity)) - { - // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); - identity.gameObject.SetActive(true); - - // fix https://github.com/vis2k/Mirror/issues/2778: - // -> SetActive(true) does NOT call Awake() if the parent - // is inactive - // -> we need Awake() to initialize NetworkBehaviours[] etc. - // because our second pass below spawns and works with it - // => detect this situation and manually call Awake for - // proper initialization - if (!identity.gameObject.activeInHierarchy) - identity.Awake(); - } + identity.assetId = assetId; } + SpawnObject(obj, ownerConnection); + } - // second pass: spawn all scene objects - foreach (NetworkIdentity identity in identities) + static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) + { + // verify if we can spawn this + if (Utils.IsPrefab(obj)) { - if (ValidateSceneObject(identity)) - // pass connection so that authority is not lost when server loads a scene - // https://github.com/vis2k/Mirror/pull/2987 - Spawn(identity.gameObject, identity.connectionToClient); + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first.", obj); + return; } - return true; - } - - static void Respawn(NetworkIdentity identity) - { - if (identity.netId == 0) + if (!active) { - // If the object has not been spawned, then do a full spawn and update observers - Spawn(identity.gameObject, identity.connectionToClient); + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server.", obj); + return; } - else + + if (!obj.TryGetComponent(out NetworkIdentity identity)) { - // otherwise just replace his data - SendSpawnMessage(identity, identity.connectionToClient); + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}", obj); + return; } - } - static void SpawnObserversForConnection(NetworkConnectionToClient conn) - { - //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); + if (identity.SpawnedFromInstantiate) + { + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; + } - if (!conn.isReady) + // Spawn should only be called once per netId. + // calling it twice would lead to undefined behaviour. + // https://github.com/MirrorNetworking/Mirror/pull/3205 + if (spawned.ContainsKey(identity.netId)) { - // client needs to finish initializing before we can spawn objects - // otherwise it would not find them. + Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned.", identity.gameObject); return; } - // let connection know that we are about to start spawning... - conn.Send(new ObjectSpawnStartedMessage()); + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; - // add connection to each nearby NetworkIdentity's observers, which - // internally sends a spawn message for each one to the connection. - foreach (NetworkIdentity identity in spawned.Values) + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.isOwned = true; + + // only call OnStartServer if not spawned yet. + // check used to be in NetworkIdentity. may not be necessary anymore. + if (!identity.isServer && identity.netId == 0) { - // try with far away ones in ummorpg! - if (identity.gameObject.activeSelf) //TODO this is different - { - //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); + // configure NetworkIdentity + // this may be called in host mode, so we need to initialize + // isLocalPlayer/isClient flags too. + identity.isLocalPlayer = NetworkClient.localPlayer == identity; + identity.isClient = NetworkClient.active; + identity.isServer = true; + identity.netId = NetworkIdentity.GetNextNetworkId(); - // we need to support three cases: - // - legacy system (identity has .visibility) - // - new system (networkserver has .aoi) - // - default case: no .visibility and no .aoi means add all - // connections by default) - // - // ForceHidden/ForceShown overwrite all systems so check it - // first! + // add to spawned (after assigning netId) + spawned[identity.netId] = identity; - // ForceShown: add no matter what - if (identity.visible == Visibility.ForceShown) - { - identity.AddObserver(conn); - } - // ForceHidden: don't show no matter what - else if (identity.visible == Visibility.ForceHidden) - { - // do nothing - } - // default: legacy system / new system / no system support - else if (identity.visible == Visibility.Default) - { - // aoi system - if (aoi != null) - { - // call OnCheckObserver - if (aoi.OnCheckObserver(identity, conn)) - identity.AddObserver(conn); - } - // no system: add all observers by default - else - { - identity.AddObserver(conn); - } - } + // callback after all fields were set + identity.OnStartServer(); + } + + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); + + if (aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); } } - // let connection know that we finished spawning, so it can call - // OnStartClient on each one (only after all were spawned, which - // is how Unity's Start() function works too) - conn.Send(new ObjectSpawnFinishedMessage()); + RebuildObservers(identity, true); } /// This takes an object that has been spawned and un-spawns it. @@ -1297,28 +1428,31 @@ static void SpawnObserversForConnection(NetworkConnectionToClient conn) public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset); // destroy ///////////////////////////////////////////////////////////// - /// Destroys all of the connection's owned objects on the server. - // This is used when a client disconnects, to remove the players for - // that client. This also destroys non-player objects that have client - // authority set for this connection. - public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) - { - // destroy all objects owned by this connection, including the player object - conn.DestroyOwnedObjects(); - // remove connection from all of its observing entities observers - // fixes https://github.com/vis2k/Mirror/issues/2737 - // -> cleaning those up in NetworkConnection.Disconnect is NOT enough - // because voluntary disconnects from the other end don't call - // NetworkConnectionn.Disconnect() - conn.RemoveFromObservingsObservers(); - conn.identity = null; - } - // sometimes we want to GameObject.Destroy it. // sometimes we want to just unspawn on clients and .Reset() it on server. // => 'bool destroy' isn't obvious enough. it's really destroy OR reset! enum DestroyMode { Destroy, Reset } + /// Destroys this object and corresponding objects on all clients. + // In some cases it is useful to remove an object but not delete it on + // the server. For that, use NetworkServer.UnSpawn() instead of + // NetworkServer.Destroy(). + public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy); + + static void DestroyObject(GameObject obj, DestroyMode mode) + { + if (obj == null) + { + Debug.Log("NetworkServer DestroyObject is null"); + return; + } + + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + DestroyObject(identity, mode); + } + } + static void DestroyObject(NetworkIdentity identity, DestroyMode mode) { // Debug.Log($"DestroyObject instance:{identity.netId}"); @@ -1346,11 +1480,14 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) identity.connectionToClient?.RemoveOwnedObject(identity); // send object destroy message to all observers, clear observers - SendToObservers(identity, new ObjectDestroyMessage{netId = identity.netId}); + SendToObservers(identity, new ObjectDestroyMessage + { + netId = identity.netId + }); identity.ClearObservers(); // in host mode, call OnStopClient/OnStopLocalPlayer manually - if (NetworkClient.active && localClientActive) + if (NetworkClient.active && activeHost) { if (identity.isLocalPlayer) identity.OnStopLocalPlayer(); @@ -1359,10 +1496,11 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // The object may have been spawned with host client ownership, // e.g. a pet so we need to clear hasAuthority and call // NotifyAuthority which invokes OnStopAuthority if hasAuthority. - identity.hasAuthority = false; + identity.isOwned = false; identity.NotifyAuthority(); // remove from NetworkClient dictionary + NetworkClient.connection.owned.Remove(identity); NetworkClient.spawned.Remove(identity.netId); } @@ -1392,30 +1530,30 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) } } - static void DestroyObject(GameObject obj, DestroyMode mode) - { - if (obj == null) - { - Debug.Log("NetworkServer DestroyObject is null"); - return; - } + // interest management ///////////////////////////////////////////////// + // Helper function to add all server connections as observers. + // This is used if none of the components provides their own + // OnRebuildObservers function. + // allocate newObservers helper HashSet only once + // internal for tests + internal static readonly HashSet newObservers = + new HashSet(); - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + // rebuild observers default method (no AOI) - adds all connections + static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) + { + // only add all connections when rebuilding the first time. + // second time we just keep them without rebuilding anything. + if (initialize) { - DestroyObject(identity, mode); + // not force hidden? + if (identity.visible != Visibility.ForceHidden) + { + AddAllReadyServerConnectionsToObservers(identity); + } } } - /// Destroys this object and corresponding objects on all clients. - // In some cases it is useful to remove an object but not delete it on - // the server. For that, use NetworkServer.UnSpawn() instead of - // NetworkServer.Destroy(). - public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy); - - // interest management ///////////////////////////////////////////////// - // Helper function to add all server connections as observers. - // This is used if none of the components provides their own - // OnRebuildObservers function. internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity) { // add all server connections @@ -1433,22 +1571,33 @@ internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity ide } } - // allocate newObservers helper HashSet only once - // internal for tests - internal static readonly HashSet newObservers = new HashSet(); - - // rebuild observers default method (no AOI) - adds all connections - static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) + // RebuildObservers does a local rebuild for the NetworkIdentity. + // This causes the set of players that can see this object to be rebuild. + // + // IMPORTANT: + // => global rebuild would be more simple, BUT + // => local rebuild is way faster for spawn/despawn because we can + // simply rebuild a select NetworkIdentity only + // => having both .observers and .observing is necessary for local + // rebuilds + // + // in other words, this is the perfect solution even though it's not + // completely simple (due to .observers & .observing) + // + // Mirror maintains .observing automatically in the background. best of + // both worlds without any worrying now! + public static void RebuildObservers(NetworkIdentity identity, bool initialize) { - // only add all connections when rebuilding the first time. - // second time we just keep them without rebuilding anything. - if (initialize) + // if there is no interest management system, + // or if 'force shown' then add all connections + if (aoi == null || identity.visible == Visibility.ForceShown) { - // not force hidden? - if (identity.visible != Visibility.ForceHidden) - { - AddAllReadyServerConnectionsToObservers(identity); - } + RebuildObserversDefault(identity, initialize); + } + // otherwise let interest management system rebuild + else + { + RebuildObserversCustom(identity, initialize); } } @@ -1502,7 +1651,7 @@ static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) { // removed observer conn.RemoveFromObserving(identity, false); - // Debug.Log($"Removed Observer for {gameObjec} {conn}"); + // Debug.Log($"Removed Observer for {gameObject} {conn}"); changed = true; } } @@ -1549,47 +1698,13 @@ static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) } } - // RebuildObservers does a local rebuild for the NetworkIdentity. - // This causes the set of players that can see this object to be rebuild. - // - // IMPORTANT: - // => global rebuild would be more simple, BUT - // => local rebuild is way faster for spawn/despawn because we can - // simply rebuild a select NetworkIdentity only - // => having both .observers and .observing is necessary for local - // rebuilds - // - // in other words, this is the perfect solution even though it's not - // completely simple (due to .observers & .observing) - // - // Mirror maintains .observing automatically in the background. best of - // both worlds without any worrying now! - public static void RebuildObservers(NetworkIdentity identity, bool initialize) - { - // observers are null until OnStartServer creates them - if (identity.observers == null) - return; - - // if there is no interest management system, - // or if 'force shown' then add all connections - if (aoi == null || identity.visible == Visibility.ForceShown) - { - RebuildObserversDefault(identity, initialize); - } - // otherwise let interest management system rebuild - else - { - RebuildObserversCustom(identity, initialize); - } - } - // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection - static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) { // get serialization for this entity (cached) // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetSerializationAtTick(Time.frameCount); + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); // is this entity owned by this connection? bool owned = identity.connectionToClient == connection; @@ -1628,7 +1743,7 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) { // get serialization for this entity viewed by this connection // (if anything was serialized this time) - NetworkWriter serialization = GetEntitySerializationForConnection(identity, connection); + NetworkWriter serialization = SerializeForConnection(identity, connection); if (serialization != null) { EntityStateMessage message = new EntityStateMessage @@ -1674,6 +1789,17 @@ static void Broadcast() // pull in UpdateVarsMessage for each entity it observes if (connection.isReady) { + // send time for snapshot interpolation every sendInterval. + // BroadcastToConnection() may not send if nothing is new. + // + // sent over unreliable. + // NetworkTime / Transform both use unreliable. + // + // make sure Broadcast() is only called every sendInterval, + // even if targetFrameRate isn't set in host mode (!) + // (done via AccurateInterval) + connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); + // broadcast world state to this connection BroadcastToConnection(connection); } @@ -1712,21 +1838,89 @@ static void Broadcast() // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkEarlyUpdate() { + // measure update time for profiling. + if (active) + { + earlyUpdateDuration.Begin(); + fullUpdateDuration.Begin(); + } + // process all incoming messages first before updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ServerEarlyUpdate(); + if (Transport.active != null) + Transport.active.ServerEarlyUpdate(); + + // step each connection's local time interpolation in early update. + foreach (NetworkConnectionToClient connection in connections.Values) + connection.UpdateTimeInterpolation(); + + if (active) earlyUpdateDuration.End(); } internal static void NetworkLateUpdate() { - // only broadcast world if active if (active) - Broadcast(); + { + // measure update time for profiling. + lateUpdateDuration.Begin(); + + // only broadcast world if active + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + if (!Application.isPlaying || +#if !UNITY_2020_3_OR_NEWER + // Unity 2019 doesn't have Time.timeAsDouble yet + AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime)) +#else + AccurateInterval.Elapsed(Time.timeAsDouble, sendInterval, ref lastSendTime)) +#endif + { + Broadcast(); + } + } // process all outgoing messages after updating the world // (even if not active. still want to process disconnects etc.) - if (Transport.activeTransport != null) - Transport.activeTransport.ServerLateUpdate(); + if (Transport.active != null) + Transport.active.ServerLateUpdate(); + + // measure actual tick rate every second. + if (active) + { + ++actualTickRateCounter; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= actualTickRateStart + 1) + { + // calculate avg by exact elapsed time. + // assuming 1s wouldn't be accurate, usually a few more ms passed. + float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); + actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); + actualTickRateStart = NetworkTime.localTime; + actualTickRateCounter = 0; + } + + // measure total update time. including transport. + // because in early update, transport update calls handlers. + lateUpdateDuration.End(); + fullUpdateDuration.End(); + } } + + // calls OnStartClient for all SERVER objects in host mode once. + // client doesn't get spawn messages for those, so need to call manually. + // Deprecated 2022-12-12 + [Obsolete("NetworkServer.ActivateHostScene was moved to HostMode.ActivateHostScene")] + public static void ActivateHostScene() => HostMode.ActivateHostScene(); } } diff --git a/Assets/Mirror/Runtime/NetworkServer.cs.meta b/Assets/Mirror/Core/NetworkServer.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkServer.cs.meta rename to Assets/Mirror/Core/NetworkServer.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs b/Assets/Mirror/Core/NetworkStartPosition.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkStartPosition.cs rename to Assets/Mirror/Core/NetworkStartPosition.cs diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta b/Assets/Mirror/Core/NetworkStartPosition.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkStartPosition.cs.meta rename to Assets/Mirror/Core/NetworkStartPosition.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkTime.cs b/Assets/Mirror/Core/NetworkTime.cs similarity index 57% rename from Assets/Mirror/Runtime/NetworkTime.cs rename to Assets/Mirror/Core/NetworkTime.cs index 1721524..04edc62 100644 --- a/Assets/Mirror/Runtime/NetworkTime.cs +++ b/Assets/Mirror/Core/NetworkTime.cs @@ -1,4 +1,9 @@ -using System; +// NetworkTime now uses NetworkClient's snapshot interpolated timeline. +// this gives ideal results & ensures everything is on the same timeline. +// previously, NetworkTransforms were on separate timelines. +// +// however, some of the old NetworkTime code remains for ping time (rtt). +// some users may still be using that. using System.Runtime.CompilerServices; using UnityEngine; #if !UNITY_2020_3_OR_NEWER @@ -11,7 +16,7 @@ namespace Mirror public static class NetworkTime { /// Ping message frequency, used to calculate network time and RTT - public static float PingFrequency = 2.0f; + public static float PingFrequency = 2; /// Average out the last few results from Ping public static int PingWindowSize = 10; @@ -19,11 +24,6 @@ public static class NetworkTime static double lastPingTime; static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(10); - static ExponentialMovingAverage _offset = new ExponentialMovingAverage(10); - - // the true offset guaranteed to be in this range - static double offsetMin = double.MinValue; - static double offsetMax = double.MaxValue; /// Returns double precision clock time _in this system_, unaffected by the network. #if UNITY_2020_3_OR_NEWER @@ -42,6 +42,8 @@ public static double localTime #endif /// The time in seconds since the server started. + // via global NetworkClient snapshot interpolated timeline (if client). + // on server, this is simply Time.timeAsDouble. // // I measured the accuracy of float and I got this: // for the same day, accuracy is better than 1 ms @@ -51,47 +53,30 @@ public static double localTime // after 60 days, accuracy is 454 ms // in other words, if the server is running for 2 months, // and you cast down to float, then the time will jump in 0.4s intervals. - // - // TODO consider using Unbatcher's remoteTime for NetworkTime public static double time { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => localTime - _offset.Value; + get => NetworkServer.active + ? localTime + : NetworkClient.localTimeline; } - /// Time measurement variance. The higher, the less accurate the time is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double timeVariance => _offset.Var; - - /// Time standard deviation. The highe, the less accurate the time is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double timeStandardDeviation => Math.Sqrt(timeVariance); - /// Clock difference in seconds between the client and the server. Always 0 on server. - public static double offset => _offset.Value; + // original implementation used 'client - server' time. keep it this way. + // TODO obsolete later. people shouldn't worry about this. + public static double offset => localTime - time; /// Round trip time (in seconds) that it takes a message to go client->server->client. public static double rtt => _rtt.Value; - /// Round trip time variance. The higher, the less accurate the rtt is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double rttVariance => _rtt.Var; - - /// Round trip time standard deviation. The higher, the less accurate the rtt is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double rttStandardDeviation => Math.Sqrt(rttVariance); - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - [UnityEngine.RuntimeInitializeOnLoadMethod] + [RuntimeInitializeOnLoadMethod] public static void ResetStatics() { - PingFrequency = 2.0f; + PingFrequency = 2; PingWindowSize = 10; lastPingTime = 0; _rtt = new ExponentialMovingAverage(PingWindowSize); - _offset = new ExponentialMovingAverage(PingWindowSize); - offsetMin = double.MinValue; - offsetMax = double.MaxValue; #if !UNITY_2020_3_OR_NEWER stopwatch.Restart(); #endif @@ -127,33 +112,9 @@ internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMes // and update time offset internal static void OnClientPong(NetworkPongMessage message) { - double now = localTime; - // how long did this message take to come back - double newRtt = now - message.clientTime; + double newRtt = localTime - message.clientTime; _rtt.Add(newRtt); - - // the difference in time between the client and the server - // but subtract half of the rtt to compensate for latency - // half of rtt is the best approximation we have - double newOffset = now - newRtt * 0.5f - message.serverTime; - - double newOffsetMin = now - newRtt - message.serverTime; - double newOffsetMax = now - message.serverTime; - offsetMin = Math.Max(offsetMin, newOffsetMin); - offsetMax = Math.Min(offsetMax, newOffsetMax); - - if (_offset.Value < offsetMin || _offset.Value > offsetMax) - { - // the old offset was offrange, throw it away and use new one - _offset = new ExponentialMovingAverage(PingWindowSize); - _offset.Add(newOffset); - } - else if (newOffset >= offsetMin || newOffset <= offsetMax) - { - // new offset looks reasonable, add to the average - _offset.Add(newOffset); - } } } } diff --git a/Assets/Mirror/Runtime/NetworkTime.cs.meta b/Assets/Mirror/Core/NetworkTime.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkTime.cs.meta rename to Assets/Mirror/Core/NetworkTime.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkWriter.cs b/Assets/Mirror/Core/NetworkWriter.cs similarity index 66% rename from Assets/Mirror/Runtime/NetworkWriter.cs rename to Assets/Mirror/Core/NetworkWriter.cs index 442075f..18bbad6 100644 --- a/Assets/Mirror/Runtime/NetworkWriter.cs +++ b/Assets/Mirror/Core/NetworkWriter.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.CompilerServices; +using System.Text; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; @@ -9,15 +10,29 @@ namespace Mirror public class NetworkWriter { public const int MaxStringLength = 1024 * 32; + public const int DefaultCapacity = 1500; // create writer immediately with it's own buffer so no one can mess with it and so that we can resize it. // note: BinaryWriter allocates too much, so we only use a MemoryStream // => 1500 bytes by default because on average, most packets will be <= MTU - byte[] buffer = new byte[1500]; + internal byte[] buffer = new byte[DefaultCapacity]; /// Next position to write to the buffer public int Position; + /// Current capacity. Automatically resized if necessary. + public int Capacity => buffer.Length; + + // cache encoding for WriteString instead of creating it each time. + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + // not(!) static for thread safety. + // + // throwOnInvalidBytes is true. + // writer should throw and user should fix if this ever happens. + // unlike reader, which needs to expect it to happen from attackers. + internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + /// Reset both the position and length of the stream // Leaves the capacity the same so that we can reuse this writer without // extra allocations @@ -31,7 +46,7 @@ public void Reset() // 1. 'has space' checks are necessary even for fixed sized writers. // 2. all writers will eventually be large enough to stop resizing. [MethodImpl(MethodImplOptions.AggressiveInlining)] - void EnsureCapacity(int value) + internal void EnsureCapacity(int value) { if (buffer.Length < value) { @@ -41,6 +56,7 @@ void EnsureCapacity(int value) } /// Copies buffer until 'Position' to a new array. + // Try to use ToArraySegment instead to avoid allocations! [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte[] ToArray() { @@ -51,10 +67,13 @@ public byte[] ToArray() /// Returns allocation-free ArraySegment until 'Position'. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArraySegment ToArraySegment() - { - return new ArraySegment(buffer, 0, Position); - } + public ArraySegment ToArraySegment() => + new ArraySegment(buffer, 0, Position); + + // implicit conversion for convenience + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ArraySegment(NetworkWriter w) => + w.ToArraySegment(); // WriteBlittable from DOTSNET. // this is extremely fast, but only works for blittable types. @@ -82,6 +101,26 @@ public ArraySegment ToArraySegment() // WriteBlittable assumes same endianness for server & client. // All Unity 2018+ platforms are little endian. // => run NetworkWriterTests.BlittableOnThisPlatform() to verify! + // + // This is not safe to expose to random structs. + // * StructLayout.Sequential is the default, which is safe. + // if the struct contains a reference type, it is converted to Auto. + // but since all structs here are unmanaged blittable, it's safe. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential + // * StructLayout.Pack depends on CPU word size. + // this may be different 4 or 8 on some ARM systems, etc. + // this is not safe, and would cause bytes/shorts etc. to be padded. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0 + // * If we force pack all to '1', they would have no padding which is + // great for bandwidth. but on some android systems, CPU can't read + // unaligned memory. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // * The only option would be to force explicit layout with multiples + // of word size. but this requires lots of weaver checking and is + // still questionable (IL2CPP etc.). + // + // Note: inlining WriteBlittable is enough. don't inline WriteInt etc. + // we don't want WriteBlittable to be copied in place everywhere. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void WriteBlittable(T value) where T : unmanaged @@ -148,18 +187,36 @@ internal void WriteBlittableNullable(T? value) WriteBlittable(value.Value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteByte(byte value) => WriteBlittable(value); // for byte arrays with consistent size, where the reader knows how many to read // (like a packet opcode that's always the same) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBytes(byte[] buffer, int offset, int count) + public void WriteBytes(byte[] array, int offset, int count) { EnsureCapacity(Position + count); - Array.ConstrainedCopy(buffer, offset, this.buffer, Position, count); + Array.ConstrainedCopy(array, offset, this.buffer, Position, count); Position += count; } + // write an unsafe byte* array. + // useful for bit tree compression, etc. + public unsafe bool WriteBytes(byte* ptr, int offset, int size) + { + EnsureCapacity(Position + size); + + fixed (byte* destination = &buffer[Position]) + { + // write 'size' bytes at position + // 10 mio writes: 868ms + // Array.Copy(value.Array, value.Offset, buffer, Position, value.Count); + // 10 mio writes: 775ms + // Buffer.BlockCopy(value.Array, value.Offset, buffer, Position, value.Count); + // 10 mio writes: 637ms + UnsafeUtility.MemCpy(destination, ptr + offset, size); + } + + Position += size; + return true; + } /// Writes any type that mirror supports. Uses weaver populated Writer(T).write. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -175,6 +232,12 @@ public void Write(T value) writeDelegate(this, value); } } + + // print with buffer content for easier debugging. + // [content, position / capacity]. + // showing "position / space" would be too confusing. + public override string ToString() => + $"[{ToArraySegment().ToHexString()} @ {Position}/{Capacity}]"; } /// Helper class that weaver populates with all writer types. diff --git a/Assets/Mirror/Runtime/NetworkWriter.cs.meta b/Assets/Mirror/Core/NetworkWriter.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkWriter.cs.meta rename to Assets/Mirror/Core/NetworkWriter.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs b/Assets/Mirror/Core/NetworkWriterExtensions.cs similarity index 72% rename from Assets/Mirror/Runtime/NetworkWriterExtensions.cs rename to Assets/Mirror/Core/NetworkWriterExtensions.cs index cf0954e..d55a7be 100644 --- a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs +++ b/Assets/Mirror/Core/NetworkWriterExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; using UnityEngine; namespace Mirror @@ -11,70 +9,41 @@ namespace Mirror // but they do all need to be extensions. public static class NetworkWriterExtensions { - // cache encoding instead of creating it with BinaryWriter each time - // 1000 readers before: 1MB GC, 30ms - // 1000 readers after: 0.8MB GC, 18ms - static readonly UTF8Encoding encoding = new UTF8Encoding(false, true); - static readonly byte[] stringBuffer = new byte[NetworkWriter.MaxStringLength]; - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value); // char is not blittable. convert to ushort. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value); // bool is not blittable. convert to byte. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?()); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value); - + [StructLayout(LayoutKind.Explicit)] internal struct UIntDouble { @@ -85,7 +54,6 @@ internal struct UIntDouble public ulong longValue; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteDouble(this NetworkWriter writer, double value) { // DEBUG: try to find the exact value that fails. @@ -95,15 +63,11 @@ public static void WriteDouble(this NetworkWriter writer, double value) writer.WriteBlittable(value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteString(this NetworkWriter writer, string value) { // write 0 for null support, increment real size by 1 @@ -116,22 +80,27 @@ public static void WriteString(this NetworkWriter writer, string value) return; } - // write string with same method as NetworkReader - // convert to byte[] - int size = encoding.GetBytes(value, 0, value.Length, stringBuffer, 0); + // WriteString copies into the buffer manually. + // need to ensure capacity here first, manually. + int maxSize = writer.encoding.GetMaxByteCount(value.Length); + writer.EnsureCapacity(writer.Position + 2 + maxSize); // 2 bytes position + N bytes encoding + + // encode it into the buffer first. + // reserve 2 bytes for header after we know how much was written. + int written = writer.encoding.GetBytes(value, 0, value.Length, writer.buffer, writer.Position + 2); // check if within max size - if (size >= NetworkWriter.MaxStringLength) - { - throw new IndexOutOfRangeException($"NetworkWriter.Write(string) too long: {size}. Limit: {NetworkWriter.MaxStringLength}"); - } + if (written >= NetworkWriter.MaxStringLength) + throw new IndexOutOfRangeException($"NetworkWriter.Write(string) too long: {written}. Limit: {NetworkWriter.MaxStringLength}"); - // write size and bytes - writer.WriteUShort(checked((ushort)(size + 1))); - writer.WriteBytes(stringBuffer, 0, size); + // .Position is unchanged, so fill in the size header now. + // we already ensured that max size fits into ushort.max-1. + writer.WriteUShort(checked((ushort)(written + 1))); // Position += 2 + + // now update position by what was written above + writer.Position += written; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteBytesAndSizeSegment(this NetworkWriter writer, ArraySegment buffer) { writer.WriteBytesAndSize(buffer.Array, buffer.Offset, buffer.Count); @@ -140,7 +109,6 @@ public static void WriteBytesAndSizeSegment(this NetworkWriter writer, ArraySegm // Weaver needs a write function with just one byte[] parameter // (we don't name it .Write(byte[]) because it's really a WriteBytesAndSize since we write size / null info too) - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer) { // buffer might be null, so we can't use .Length in that case @@ -150,7 +118,6 @@ public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer) // for byte arrays with dynamic size, where the reader doesn't know how many will come // (like an inventory with different items etc.) - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, int offset, int count) { // null is supported because [SyncVar]s might be structs with null byte[] arrays @@ -165,7 +132,6 @@ public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, i writer.WriteBytes(buffer, offset, count); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteArraySegment(this NetworkWriter writer, ArraySegment segment) { int length = segment.Count; @@ -176,81 +142,64 @@ public static void WriteArraySegment(this NetworkWriter writer, ArraySegment< } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteRect(this NetworkWriter writer, Rect value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteRectNullable(this NetworkWriter writer, Rect? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WritePlane(this NetworkWriter writer, Plane value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WritePlaneNullable(this NetworkWriter writer, Plane? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteRay(this NetworkWriter writer, Ray value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteRayNullable(this NetworkWriter writer, Ray? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteGuid(this NetworkWriter writer, Guid value) { +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have Span yet byte[] data = value.ToByteArray(); writer.WriteBytes(data, 0, data.Length); +#else + // WriteBlittable(Guid) isn't safe. see WriteBlittable comments. + // Guid is Sequential, but we can't guarantee packing. + // TryWriteBytes is safe and allocation free. + writer.EnsureCapacity(writer.Position + 16); + value.TryWriteBytes(new Span(writer.buffer, writer.Position, 16)); + writer.Position += 16; +#endif } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteGuidNullable(this NetworkWriter writer, Guid? value) { writer.WriteBool(value.HasValue); if (value.HasValue) writer.WriteGuid(value.Value); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value) { if (value == null) @@ -272,7 +221,6 @@ public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdenti writer.WriteUInt(value.netId); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value) { if (value == null) @@ -281,10 +229,9 @@ public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehav return; } writer.WriteUInt(value.netId); - writer.WriteByte((byte)value.ComponentIndex); + writer.WriteByte(value.ComponentIndex); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteTransform(this NetworkWriter writer, Transform value) { if (value == null) @@ -292,8 +239,7 @@ public static void WriteTransform(this NetworkWriter writer, Transform value) writer.WriteUInt(0); return; } - NetworkIdentity identity = value.GetComponent(); - if (identity != null) + if (value.TryGetComponent(out NetworkIdentity identity)) { writer.WriteUInt(identity.netId); } @@ -304,7 +250,6 @@ public static void WriteTransform(this NetworkWriter writer, Transform value) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteGameObject(this NetworkWriter writer, GameObject value) { if (value == null) @@ -314,8 +259,7 @@ public static void WriteGameObject(this NetworkWriter writer, GameObject value) } // warn if the GameObject doesn't have a NetworkIdentity, - NetworkIdentity identity = value.GetComponent(); - if (identity == null) + if (!value.TryGetComponent(out NetworkIdentity identity)) Debug.LogWarning($"NetworkWriter {value} has no NetworkIdentity"); // serialize the correct amount of data in any case to make sure @@ -323,7 +267,10 @@ public static void WriteGameObject(this NetworkWriter writer, GameObject value) writer.WriteNetworkIdentity(identity); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // while SyncList is recommended for NetworkBehaviours, + // structs may have .List members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Writers/GenerateWriter() handles this manually. public static void WriteList(this NetworkWriter writer, List list) { if (list is null) @@ -336,7 +283,25 @@ public static void WriteList(this NetworkWriter writer, List list) writer.Write(list[i]); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // while SyncSet is recommended for NetworkBehaviours, + // structs may have .Set members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Writers/GenerateWriter() handles this manually. + // TODO writer not found. need to adjust weaver first. see tests. + /* + public static void WriteHashSet(this NetworkWriter writer, HashSet hashSet) + { + if (hashSet is null) + { + writer.WriteInt(-1); + return; + } + writer.WriteInt(hashSet.Count); + foreach (T item in hashSet) + writer.Write(item); + } + */ + public static void WriteArray(this NetworkWriter writer, T[] array) { if (array is null) @@ -349,21 +314,43 @@ public static void WriteArray(this NetworkWriter writer, T[] array) writer.Write(array[i]); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUri(this NetworkWriter writer, Uri uri) { writer.WriteString(uri?.ToString()); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D) { + // TODO allocation protection when sending textures to server. + // currently can allocate 32k x 32k x 4 byte = 3.8 GB + + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + // simply send -1 for width. + if (texture2D == null) + { + writer.WriteShort(-1); + return; + } + + // write dimensions first so reader can create the texture with size + // 32k x 32k short is more than enough + writer.WriteShort((short)texture2D.width); + writer.WriteShort((short)texture2D.height); writer.WriteArray(texture2D.GetPixels32()); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteSprite(this NetworkWriter writer, Sprite sprite) { + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + // simply send a 'null' for texture content. + if (sprite == null) + { + writer.WriteTexture2D(null); + return; + } + writer.WriteTexture2D(sprite.texture); writer.WriteRect(sprite.rect); writer.WriteVector2(sprite.pivot); diff --git a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta b/Assets/Mirror/Core/NetworkWriterExtensions.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta rename to Assets/Mirror/Core/NetworkWriterExtensions.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs b/Assets/Mirror/Core/NetworkWriterPool.cs similarity index 80% rename from Assets/Mirror/Runtime/NetworkWriterPool.cs rename to Assets/Mirror/Core/NetworkWriterPool.cs index c63323d..693517f 100644 --- a/Assets/Mirror/Runtime/NetworkWriterPool.cs +++ b/Assets/Mirror/Core/NetworkWriterPool.cs @@ -1,5 +1,4 @@ // API consistent with Microsoft's ObjectPool. -using System; using System.Runtime.CompilerServices; namespace Mirror @@ -19,10 +18,6 @@ public static class NetworkWriterPool 1000 ); - // DEPRECATED 2022-03-10 - [Obsolete("GetWriter() was renamed to Get()")] - public static NetworkWriterPooled GetWriter() => Get(); - /// Get a writer from the pool. Creates new one if pool is empty. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkWriterPooled Get() @@ -33,10 +28,6 @@ public static NetworkWriterPooled Get() return writer; } - // DEPRECATED 2022-03-10 - [Obsolete("Recycle() was renamed to Return()")] - public static void Recycle(NetworkWriterPooled writer) => Return(writer); - /// Return a writer to the pool. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Return(NetworkWriterPooled writer) diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta b/Assets/Mirror/Core/NetworkWriterPool.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkWriterPool.cs.meta rename to Assets/Mirror/Core/NetworkWriterPool.cs.meta diff --git a/Assets/Mirror/Runtime/NetworkWriterPooled.cs b/Assets/Mirror/Core/NetworkWriterPooled.cs similarity index 69% rename from Assets/Mirror/Runtime/NetworkWriterPooled.cs rename to Assets/Mirror/Core/NetworkWriterPooled.cs index ce113bc..5ac371d 100644 --- a/Assets/Mirror/Runtime/NetworkWriterPooled.cs +++ b/Assets/Mirror/Core/NetworkWriterPooled.cs @@ -4,10 +4,6 @@ namespace Mirror { - // DEPRECATED 2022-03-10 - [Obsolete("PooledNetworkWriter was renamed to NetworkWriterPooled. It's cleaner & slightly easier to use.")] - public sealed class PooledNetworkWriter : NetworkWriterPooled {} - /// Pooled NetworkWriter, automatically returned to pool when using 'using' // TODO make sealed again after removing obsolete NetworkWriterPooled! public class NetworkWriterPooled : NetworkWriter, IDisposable diff --git a/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta b/Assets/Mirror/Core/NetworkWriterPooled.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta rename to Assets/Mirror/Core/NetworkWriterPooled.cs.meta diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs b/Assets/Mirror/Core/RemoteCalls.cs similarity index 81% rename from Assets/Mirror/Runtime/RemoteCalls.cs rename to Assets/Mirror/Core/RemoteCalls.cs index 127e241..ffae3a0 100644 --- a/Assets/Mirror/Runtime/RemoteCalls.cs +++ b/Assets/Mirror/Core/RemoteCalls.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using UnityEngine; namespace Mirror.RemoteCalls @@ -38,13 +37,14 @@ public static class RemoteProcedureCalls // IMPORTANT: cmd/rpc functions are identified via **HASHES**. // an index would requires half the bandwidth, but introduces issues // where static constructors are lazily called, so index order isn't - // guaranteed: + // guaranteed. keep hashes to avoid: // https://github.com/vis2k/Mirror/pull/3135 // https://github.com/vis2k/Mirror/issues/3138 - // keep the 4 byte hash for stability! - static readonly Dictionary remoteCallDelegates = new Dictionary(); + // BUT: 2 byte hash is enough if we check for collisions. that's what we + // do for NetworkMessage as well. + static readonly Dictionary remoteCallDelegates = new Dictionary(); - static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, int functionHash) + static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash) { if (remoteCallDelegates.ContainsKey(functionHash)) { @@ -58,17 +58,17 @@ static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallT // otherwise notify user. there is a rare chance of string // hash collisions. - Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them"); + Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them. To save bandwidth, we only use 2 bytes for the hash, which has a small chance of collisions."); } return false; } // pass full function name to avoid ClassA.Func & ClassB.Func collisions - internal static int RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) + internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) { // type+func so Inventory.RpcUse != Equipment.RpcUse - int hash = functionFullName.GetStableHashCode(); + ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF); if (CheckIfDelegateExists(componentType, remoteCallType, func, hash)) return hash; @@ -96,19 +96,19 @@ public static void RegisterRpc(Type componentType, string functionFullName, Remo RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func); // to clean up tests - internal static void RemoveDelegate(int hash) => + internal static void RemoveDelegate(ushort hash) => remoteCallDelegates.Remove(hash); // note: no need to throw an error if not found. // an attacker might just try to call a cmd with an rpc's hash etc. // returning false is enough. - static bool GetInvokerForHash(int functionHash, RemoteCallType remoteCallType, out Invoker invoker) => + static bool GetInvokerForHash(ushort functionHash, RemoteCallType remoteCallType, out Invoker invoker) => remoteCallDelegates.TryGetValue(functionHash, out invoker) && invoker != null && invoker.callType == remoteCallType; // InvokeCmd/Rpc Delegate can all use the same function here - internal static bool Invoke(int functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null) + internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null) { // IMPORTANT: we check if the message's componentIndex component is // actually of the right type. prevents attackers trying @@ -124,12 +124,12 @@ internal static bool Invoke(int functionHash, RemoteCallType remoteCallType, Net } // check if the command 'requiresAuthority' which is set in the attribute - internal static bool CommandRequiresAuthority(int cmdHash) => + internal static bool CommandRequiresAuthority(ushort cmdHash) => GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) && invoker.cmdRequiresAuthority; /// Gets the handler function by hash. Useful for profilers and debuggers. - public static RemoteCallDelegate GetDelegate(int functionHash) => + public static RemoteCallDelegate GetDelegate(ushort functionHash) => remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker) ? invoker.function : null; diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs.meta b/Assets/Mirror/Core/RemoteCalls.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/RemoteCalls.cs.meta rename to Assets/Mirror/Core/RemoteCalls.cs.meta diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation.meta b/Assets/Mirror/Core/SnapshotInterpolation.meta similarity index 100% rename from Assets/Mirror/Runtime/SnapshotInterpolation.meta rename to Assets/Mirror/Core/SnapshotInterpolation.meta diff --git a/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs new file mode 100644 index 0000000..6b8ba8a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs @@ -0,0 +1,17 @@ +// Snapshot interface so we can reuse it for all kinds of systems. +// for example, NetworkTransform, NetworkRigidbody, CharacterController etc. +// NOTE: we use '' and 'where T : Snapshot' to avoid boxing. +// List would cause allocations through boxing. +namespace Mirror +{ + public interface Snapshot + { + // the remote timestamp (when it was sent by the remote) + double remoteTime { get; set; } + + // the local timestamp (when it was received on our end) + // technically not needed for basic snapshot interpolation. + // only for dynamic buffer time adjustment. + double localTime { get; set; } + } +} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta rename to Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs new file mode 100644 index 0000000..d96e10a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs @@ -0,0 +1,347 @@ +// snapshot interpolation V2 by mischa +// +// Unity independent to be engine agnostic & easy to test. +// boxing: in C#, uses does not box! passing the interface would box! +// +// credits: +// glenn fiedler: https://gafferongames.com/post/snapshot_interpolation/ +// fholm: netcode streams +// fakebyte: standard deviation for dynamic adjustment +// ninjakicka: math & debugging +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class SortedListExtensions + { + // removes the first 'amount' elements from the sorted list + public static void RemoveRange(this SortedList list, int amount) + { + // remove the first element 'amount' times. + // handles -1 and > count safely. + for (int i = 0; i < amount && i < list.Count; ++i) + list.RemoveAt(0); + } + } + + public static class SnapshotInterpolation + { + // calculate timescale for catch-up / slow-down + // note that negative threshold should be <0. + // caller should verify (i.e. Unity OnValidate). + // improves branch prediction. + public static double Timescale( + double drift, // how far we are off from bufferTime + double catchupSpeed, // in % [0,1] + double slowdownSpeed, // in % [0,1] + double catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) + double catchupPositiveThreshold) // in % of sendInterval) + { + // if the drift time is too large, it means we are behind more time. + // so we need to speed up the timescale. + // note the threshold should be sendInterval * catchupThreshold. + if (drift > catchupPositiveThreshold) + { + // localTimeline += 0.001; // too simple, this would ping pong + return 1 + catchupSpeed; // n% faster + } + + // if the drift time is too small, it means we are ahead of time. + // so we need to slow down the timescale. + // note the threshold should be sendInterval * catchupThreshold. + if (drift < catchupNegativeThreshold) + { + // localTimeline -= 0.001; // too simple, this would ping pong + return 1 - slowdownSpeed; // n% slower + } + + // keep constant timescale while within threshold. + // this way we have perfectly smooth speed most of the time. + return 1; + } + + // calculate dynamic buffer time adjustment + public static double DynamicAdjustment( + double sendInterval, + double jitterStandardDeviation, + double dynamicAdjustmentTolerance) + { + // jitter is equal to delivery time standard variation. + // delivery time is made up of 'sendInterval+jitter'. + // .Average would be dampened by the constant sendInterval + // .StandardDeviation is the changes in 'jitter' that we want + // so add it to send interval again. + double intervalWithJitter = sendInterval + jitterStandardDeviation; + + // how many multiples of sendInterval is that? + // we want to convert to bufferTimeMultiplier later. + double multiples = intervalWithJitter / sendInterval; + + // add the tolerance + double safezone = multiples + dynamicAdjustmentTolerance; + // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}"); + return safezone; + } + + // helper function to insert a snapshot if it doesn't exist yet. + // extra function so we can use it for both cases: + // NetworkClient global timeline insertions & adjustments via Insert. + // NetworkBehaviour local insertion without any time adjustments. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool InsertIfNotExists( + SortedList buffer, // snapshot buffer + T snapshot) // the newly received snapshot + where T : Snapshot + { + // SortedList does not allow duplicates. + // we don't need to check ContainsKey (which is expensive). + // simply add and compare count before/after for the return value. + + //if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive + // buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists + + int before = buffer.Count; + buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists + return buffer.Count > before; + } + + // call this for every received snapshot. + // adds / inserts it to the list & initializes local time if needed. + public static void InsertAndAdjust( + SortedList buffer, // snapshot buffer + T snapshot, // the newly received snapshot + ref double localTimeline, // local interpolation time based on server time + ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time + float sendInterval, // for debugging + double bufferTime, // offset for buffering + double catchupSpeed, // in % [0,1] + double slowdownSpeed, // in % [0,1] + ref ExponentialMovingAverage driftEma, // for catchup / slowdown + float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) + float catchupPositiveThreshold, // in % of sendInterval + ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment + where T : Snapshot + { + // first snapshot? + // initialize local timeline. + // we want it to be behind by 'offset'. + // + // note that the first snapshot may be a lagging packet. + // so we would always be behind by that lag. + // this requires catchup later. + if (buffer.Count == 0) + localTimeline = snapshot.remoteTime - bufferTime; + + // insert into the buffer. + // + // note that we might insert it between our current interpolation + // which is fine, it adds another data point for accuracy. + // + // note that insert may be called twice for the same key. + // by default, this would throw. + // need to handle it silently. + if (InsertIfNotExists(buffer, snapshot)) + { + // dynamic buffer adjustment needs delivery interval jitter + if (buffer.Count >= 2) + { + // note that this is not entirely accurate for scrambled inserts. + // + // we always use the last two, not what we just inserted + // even if we were to use the diff for what we just inserted, + // a scrambled insert would still not be 100% accurate: + // => assume a buffer of AC, with delivery time C-A + // => we then insert B, with delivery time B-A + // => but then technically the first C-A wasn't correct, + // as it would have to be C-B + // + // in practice, scramble is rare and won't make much difference + double previousLocalTime = buffer.Values[buffer.Count - 2].localTime; + double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime; + + // this is the delivery time since last snapshot + double localDeliveryTime = lastestLocalTime - previousLocalTime; + + // feed the local delivery time to the EMA. + // this is what the original stream did too. + // our final dynamic buffer adjustment is different though. + // we use standard deviation instead of average. + deliveryTimeEma.Add(localDeliveryTime); + } + + // adjust timescale to catch up / slow down after each insertion + // because that is when we add new values to our EMA. + + // we want localTimeline to be about 'bufferTime' behind. + // for that, we need the delivery time EMA. + // snapshots may arrive out of order, we can not use last-timeline. + // we need to use the inserted snapshot's time - timeline. + double latestRemoteTime = snapshot.remoteTime; + double timeDiff = latestRemoteTime - localTimeline; + + // next, calculate average of a few seconds worth of timediffs. + // this gives smoother results. + // + // to calculate the average, we could simply loop through the + // last 'n' seconds worth of timediffs, but: + // - our buffer may only store a few snapshots (bufferTime) + // - looping through seconds worth of snapshots every time is + // expensive + // + // to solve this, we use an exponential moving average. + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // which is basically fancy math to do the same but faster. + // additionally, it allows us to look at more timeDiff values + // than we sould have access to in our buffer :) + driftEma.Add(timeDiff); + + // next up, calculate how far we are currently away from bufferTime + double drift = driftEma.Value - bufferTime; + + // convert relative thresholds to absolute values based on sendInterval + double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold; + double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold; + + // next, set localTimescale to catchup consistently in Update(). + // we quantize between default/catchup/slowdown, + // this way we have 'default' speed most of the time(!). + // and only catch up / slow down for a little bit occasionally. + // a consistent multiplier would never be exactly 1.0. + localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold); + + // debug logging + // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}"); + } + } + + // sample snapshot buffer to find the pair around the given time. + // returns indices so we can use it with RemoveRange to clear old snaps. + // make sure to use use buffer.Values[from/to], not buffer[from/to]. + // make sure to only call this is we have > 0 snapshots. + public static void Sample( + SortedList buffer, // snapshot buffer + double localTimeline, // local interpolation time based on server time + out int from, // the snapshot <= time + out int to, // the snapshot >= time + out double t) // interpolation factor + where T : Snapshot + { + from = -1; + to = -1; + t = 0; + + // sample from [0,count-1] so we always have two at 'i' and 'i+1'. + for (int i = 0; i < buffer.Count - 1; ++i) + { + // is local time between these two? + T first = buffer.Values[i]; + T second = buffer.Values[i + 1]; + if (localTimeline >= first.remoteTime && + localTimeline <= second.remoteTime) + { + // use these two snapshots + from = i; + to = i + 1; + t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline); + return; + } + } + + // didn't find two snapshots around local time. + // so pick either the first or last, depending on which is closer. + + // oldest snapshot ahead of local time? + if (buffer.Values[0].remoteTime > localTimeline) + { + from = to = 0; + t = 0; + } + // otherwise initialize both to the last one + else + { + from = to = buffer.Count - 1; + t = 0; + } + } + + // progress local timeline every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // decoupled from Step for easier testing and so we can progress + // time only once in NetworkClient, while stepping for each component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StepTime( + double deltaTime, // engine delta time (unscaled) + ref double localTimeline, // local interpolation time based on server time + double localTimescale) // catchup / slowdown is applied to time every update) + { + // move local forward in time, scaled with catchup / slowdown applied + localTimeline += deltaTime * localTimescale; + } + + // sample, clear old. + // call this every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // returns true if there is anything to apply (requires at least 1 snap) + // from/to/t are out parameters instead of an interpolated 'computed'. + // this allows us to store from/to/t globally (i.e. in NetworkClient) + // and have each component apply the interpolation manually. + // besides, passing "Func Interpolate" would allocate anyway. + public static void StepInterpolation( + SortedList buffer, // snapshot buffer + double localTimeline, // local interpolation time based on server time + out T fromSnapshot, // we interpolate 'from' this snapshot + out T toSnapshot, // 'to' this snapshot + out double t) // at ratio 't' [0,1] + where T : Snapshot + { + // check this in caller: + // nothing to do if there are no snapshots at all yet + // if (buffer.Count == 0) return false; + + // sample snapshot buffer at local interpolation time + Sample(buffer, localTimeline, out int from, out int to, out t); + + // save from/to + fromSnapshot = buffer.Values[from]; + toSnapshot = buffer.Values[to]; + + // remove older snapshots that we definitely don't need anymore. + // after(!) using the indices. + // + // if we have 3 snapshots and we are between 2nd and 3rd: + // from = 1, to = 2 + // then we need to remove the first one, which is exactly 'from'. + // because 'from-1' = 0 would remove none. + buffer.RemoveRange(from); + } + + // update time, sample, clear old. + // call this every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // returns true if there is anything to apply (requires at least 1 snap) + // from/to/t are out parameters instead of an interpolated 'computed'. + // this allows us to store from/to/t globally (i.e. in NetworkClient) + // and have each component apply the interpolation manually. + // besides, passing "Func Interpolate" would allocate anyway. + public static void Step( + SortedList buffer, // snapshot buffer + double deltaTime, // engine delta time (unscaled) + ref double localTimeline, // local interpolation time based on server time + double localTimescale, // catchup / slowdown is applied to time every update + out T fromSnapshot, // we interpolate 'from' this snapshot + out T toSnapshot, // 'to' this snapshot + out double t) // at ratio 't' [0,1] + where T : Snapshot + { + StepTime(deltaTime, ref localTimeline, localTimescale); + StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t); + } + } +} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta rename to Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta diff --git a/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs new file mode 100644 index 0000000..5bfdd3a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs @@ -0,0 +1,15 @@ +namespace Mirror +{ + // empty snapshot that is only used to progress client's local timeline. + public struct TimeSnapshot : Snapshot + { + public double remoteTime { get; set; } + public double localTime { get; set; } + + public TimeSnapshot(double remoteTime, double localTime) + { + this.remoteTime = remoteTime; + this.localTime = localTime; + } + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta new file mode 100644 index 0000000..72fd450 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: afe2b5ed49634971a2aec720ad74e5cd +timeCreated: 1666288442 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs b/Assets/Mirror/Core/SyncDictionary.cs similarity index 79% rename from Assets/Mirror/Runtime/SyncDictionary.cs rename to Assets/Mirror/Core/SyncDictionary.cs index c63077c..fc404d8 100644 --- a/Assets/Mirror/Runtime/SyncDictionary.cs +++ b/Assets/Mirror/Core/SyncDictionary.cs @@ -10,7 +10,7 @@ public class SyncIDictionary : SyncObject, IDictionary objects; public int Count => objects.Count; - public bool IsReadOnly { get; private set; } + public bool IsReadOnly => !IsWritable(); public event SyncDictionaryChanged Callback; public enum Operation : byte @@ -43,7 +43,6 @@ struct Change public override void Reset() { - IsReadOnly = false; changes.Clear(); changesAhead = 0; objects.Clear(); @@ -66,11 +65,11 @@ public SyncIDictionary(IDictionary objects) this.objects = objects; } - void AddOperation(Operation op, TKey key, TValue item) + void AddOperation(Operation op, TKey key, TValue item, bool checkAccess) { - if (IsReadOnly) + if (checkAccess && IsReadOnly) { - throw new System.InvalidOperationException("SyncDictionaries can only be modified by the server"); + throw new System.InvalidOperationException("SyncDictionaries can only be modified by the owner."); } Change change = new Change @@ -133,9 +132,6 @@ public override void OnSerializeDelta(NetworkWriter writer) public override void OnDeserializeAll(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - // if init, write the full list content int count = (int)reader.ReadUInt(); @@ -157,9 +153,6 @@ public override void OnDeserializeAll(NetworkReader reader) public override void OnDeserializeDelta(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - int changesCount = (int)reader.ReadUInt(); for (int i = 0; i < changesCount; i++) @@ -180,7 +173,20 @@ public override void OnDeserializeDelta(NetworkReader reader) item = reader.Read(); if (apply) { - objects[key] = item; + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + if (ContainsKey(key)) + { + objects[key] = item; // assign after ContainsKey check + AddOperation(Operation.OP_SET, key, item, false); + } + else + { + objects[key] = item; // assign after ContainsKey check + AddOperation(Operation.OP_ADD, key, item, false); + } } break; @@ -188,6 +194,11 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Clear(); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, default, default, false); } break; @@ -196,18 +207,21 @@ public override void OnDeserializeDelta(NetworkReader reader) item = reader.Read(); if (apply) { - objects.Remove(key); + if (objects.Remove(key)) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVE, key, item, false); + } } break; } - if (apply) - { - Callback?.Invoke(operation, key, item); - } - // we just skipped this change - else + if (!apply) { + // we just skipped this change changesAhead--; } } @@ -216,7 +230,7 @@ public override void OnDeserializeDelta(NetworkReader reader) public void Clear() { objects.Clear(); - AddOperation(Operation.OP_CLEAR, default, default); + AddOperation(Operation.OP_CLEAR, default, default, true); } public bool ContainsKey(TKey key) => objects.ContainsKey(key); @@ -225,7 +239,7 @@ public bool Remove(TKey key) { if (objects.TryGetValue(key, out TValue item) && objects.Remove(key)) { - AddOperation(Operation.OP_REMOVE, key, item); + AddOperation(Operation.OP_REMOVE, key, item, true); return true; } return false; @@ -239,12 +253,12 @@ public TValue this[TKey i] if (ContainsKey(i)) { objects[i] = value; - AddOperation(Operation.OP_SET, i, value); + AddOperation(Operation.OP_SET, i, value, true); } else { objects[i] = value; - AddOperation(Operation.OP_ADD, i, value); + AddOperation(Operation.OP_ADD, i, value, true); } } } @@ -254,7 +268,7 @@ public TValue this[TKey i] public void Add(TKey key, TValue value) { objects.Add(key, value); - AddOperation(Operation.OP_ADD, key, value); + AddOperation(Operation.OP_ADD, key, value, true); } public void Add(KeyValuePair item) => Add(item.Key, item.Value); @@ -288,7 +302,7 @@ public bool Remove(KeyValuePair item) bool result = objects.Remove(item.Key); if (result) { - AddOperation(Operation.OP_REMOVE, item.Key, item.Value); + AddOperation(Operation.OP_REMOVE, item.Key, item.Value, true); } return result; } diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs.meta b/Assets/Mirror/Core/SyncDictionary.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncDictionary.cs.meta rename to Assets/Mirror/Core/SyncDictionary.cs.meta diff --git a/Assets/Mirror/Runtime/SyncList.cs b/Assets/Mirror/Core/SyncList.cs similarity index 83% rename from Assets/Mirror/Runtime/SyncList.cs rename to Assets/Mirror/Core/SyncList.cs index 9eb0a59..8b70eed 100644 --- a/Assets/Mirror/Runtime/SyncList.cs +++ b/Assets/Mirror/Core/SyncList.cs @@ -12,7 +12,7 @@ public class SyncList : SyncObject, IList, IReadOnlyList readonly IEqualityComparer comparer; public int Count => objects.Count; - public bool IsReadOnly { get; private set; } + public bool IsReadOnly => !IsWritable(); public event SyncListChanged Callback; public enum Operation : byte @@ -63,17 +63,16 @@ public SyncList(IList objects, IEqualityComparer comparer = null) public override void Reset() { - IsReadOnly = false; changes.Clear(); changesAhead = 0; objects.Clear(); } - void AddOperation(Operation op, int itemIndex, T oldItem, T newItem) + void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkAccess) { - if (IsReadOnly) + if (checkAccess && IsReadOnly) { - throw new InvalidOperationException("Synclists can only be modified at the server"); + throw new InvalidOperationException("Synclists can only be modified by the owner."); } Change change = new Change @@ -144,9 +143,6 @@ public override void OnSerializeDelta(NetworkWriter writer) public override void OnDeserializeAll(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - // if init, write the full list content int count = (int)reader.ReadUInt(); @@ -167,9 +163,6 @@ public override void OnDeserializeAll(NetworkReader reader) public override void OnDeserializeDelta(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - int changesCount = (int)reader.ReadUInt(); for (int i = 0; i < changesCount; i++) @@ -191,6 +184,11 @@ public override void OnDeserializeDelta(NetworkReader reader) { index = objects.Count; objects.Add(newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_ADD, objects.Count - 1, default, newItem, false); } break; @@ -198,6 +196,11 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Clear(); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, 0, default, default, false); } break; @@ -207,6 +210,11 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Insert(index, newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_INSERT, index, default, newItem, false); } break; @@ -216,6 +224,11 @@ public override void OnDeserializeDelta(NetworkReader reader) { oldItem = objects[index]; objects.RemoveAt(index); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, false); } break; @@ -226,17 +239,18 @@ public override void OnDeserializeDelta(NetworkReader reader) { oldItem = objects[index]; objects[index] = newItem; + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_SET, index, oldItem, newItem, false); } break; } - if (apply) - { - Callback?.Invoke(operation, index, oldItem, newItem); - } - // we just skipped this change - else + if (!apply) { + // we just skipped this change changesAhead--; } } @@ -245,7 +259,7 @@ public override void OnDeserializeDelta(NetworkReader reader) public void Add(T item) { objects.Add(item); - AddOperation(Operation.OP_ADD, objects.Count - 1, default, item); + AddOperation(Operation.OP_ADD, objects.Count - 1, default, item, true); } public void AddRange(IEnumerable range) @@ -259,7 +273,7 @@ public void AddRange(IEnumerable range) public void Clear() { objects.Clear(); - AddOperation(Operation.OP_CLEAR, 0, default, default); + AddOperation(Operation.OP_CLEAR, 0, default, default, true); } public bool Contains(T item) => IndexOf(item) >= 0; @@ -300,7 +314,7 @@ public List FindAll(Predicate match) public void Insert(int index, T item) { objects.Insert(index, item); - AddOperation(Operation.OP_INSERT, index, default, item); + AddOperation(Operation.OP_INSERT, index, default, item, true); } public void InsertRange(int index, IEnumerable range) @@ -327,7 +341,7 @@ public void RemoveAt(int index) { T oldItem = objects[index]; objects.RemoveAt(index); - AddOperation(Operation.OP_REMOVEAT, index, oldItem, default); + AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, true); } public int RemoveAll(Predicate match) @@ -354,7 +368,7 @@ public T this[int i] { T oldItem = objects[i]; objects[i] = value; - AddOperation(Operation.OP_SET, i, oldItem, value); + AddOperation(Operation.OP_SET, i, oldItem, value, true); } } } diff --git a/Assets/Mirror/Runtime/SyncList.cs.meta b/Assets/Mirror/Core/SyncList.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncList.cs.meta rename to Assets/Mirror/Core/SyncList.cs.meta diff --git a/Assets/Mirror/Runtime/SyncObject.cs b/Assets/Mirror/Core/SyncObject.cs similarity index 83% rename from Assets/Mirror/Runtime/SyncObject.cs rename to Assets/Mirror/Core/SyncObject.cs index 7df3b67..405e240 100644 --- a/Assets/Mirror/Runtime/SyncObject.cs +++ b/Assets/Mirror/Core/SyncObject.cs @@ -17,22 +17,24 @@ public abstract class SyncObject /// Used internally to check if we are currently tracking changes. // prevents ever growing .changes lists: - // if a monster has no observers but we keep modifing a SyncObject, + // if a monster has no observers but we keep modifying a SyncObject, // then the changes would never be flushed and keep growing, // because OnSerialize isn't called without observers. // => Func so we can set it to () => observers.Count > 0 // without depending on NetworkComponent/NetworkIdentity here. - // => virtual so it sipmly always records by default + // => virtual so it simply always records by default public Func IsRecording = () => true; + // SyncList/Set/etc. shouldn't be modifiable if not owned. + // otherwise they would silently get out of sync. + // need a lambda because InitSyncObject is called in ctor, when + // 'isClient' etc. aren't initialized yet. + public Func IsWritable = () => true; + /// Discard all the queued changes // Consider the object fully synchronized with clients public abstract void ClearChanges(); - // Deprecated 2021-09-17 - [Obsolete("Deprecated: Use ClearChanges instead.")] - public void Flush() => ClearChanges(); - /// Write a full copy of the object public abstract void OnSerializeAll(NetworkWriter writer); diff --git a/Assets/Mirror/Runtime/SyncObject.cs.meta b/Assets/Mirror/Core/SyncObject.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncObject.cs.meta rename to Assets/Mirror/Core/SyncObject.cs.meta diff --git a/Assets/Mirror/Runtime/SyncSet.cs b/Assets/Mirror/Core/SyncSet.cs similarity index 84% rename from Assets/Mirror/Runtime/SyncSet.cs rename to Assets/Mirror/Core/SyncSet.cs index 94e353f..13d4302 100644 --- a/Assets/Mirror/Runtime/SyncSet.cs +++ b/Assets/Mirror/Core/SyncSet.cs @@ -11,7 +11,7 @@ public class SyncSet : SyncObject, ISet protected readonly ISet objects; public int Count => objects.Count; - public bool IsReadOnly { get; private set; } + public bool IsReadOnly => !IsWritable(); public event SyncSetChanged Callback; public enum Operation : byte @@ -47,7 +47,6 @@ public SyncSet(ISet objects) public override void Reset() { - IsReadOnly = false; changes.Clear(); changesAhead = 0; objects.Clear(); @@ -57,11 +56,11 @@ public override void Reset() // this should be called after a successful sync public override void ClearChanges() => changes.Clear(); - void AddOperation(Operation op, T item) + void AddOperation(Operation op, T item, bool checkAccess) { - if (IsReadOnly) + if (checkAccess && IsReadOnly) { - throw new InvalidOperationException("SyncSets can only be modified at the server"); + throw new InvalidOperationException("SyncSets can only be modified by the owner."); } Change change = new Change @@ -79,7 +78,7 @@ void AddOperation(Operation op, T item) Callback?.Invoke(op, item); } - void AddOperation(Operation op) => AddOperation(op, default); + void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, checkAccess); public override void OnSerializeAll(NetworkWriter writer) { @@ -126,9 +125,6 @@ public override void OnSerializeDelta(NetworkWriter writer) public override void OnDeserializeAll(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - // if init, write the full list content int count = (int)reader.ReadUInt(); @@ -149,9 +145,6 @@ public override void OnDeserializeAll(NetworkReader reader) public override void OnDeserializeDelta(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - int changesCount = (int)reader.ReadUInt(); for (int i = 0; i < changesCount; i++) @@ -170,6 +163,11 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Add(item); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_ADD, item, false); } break; @@ -177,6 +175,11 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Clear(); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, false); } break; @@ -185,17 +188,18 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Remove(item); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVE, item, false); } break; } - if (apply) - { - Callback?.Invoke(operation, item); - } - // we just skipped this change - else + if (!apply) { + // we just skipped this change changesAhead--; } } @@ -205,7 +209,7 @@ public bool Add(T item) { if (objects.Add(item)) { - AddOperation(Operation.OP_ADD, item); + AddOperation(Operation.OP_ADD, item, true); return true; } return false; @@ -215,14 +219,14 @@ void ICollection.Add(T item) { if (objects.Add(item)) { - AddOperation(Operation.OP_ADD, item); + AddOperation(Operation.OP_ADD, item, true); } } public void Clear() { objects.Clear(); - AddOperation(Operation.OP_CLEAR); + AddOperation(Operation.OP_CLEAR, true); } public bool Contains(T item) => objects.Contains(item); @@ -233,7 +237,7 @@ public bool Remove(T item) { if (objects.Remove(item)) { - AddOperation(Operation.OP_REMOVE, item); + AddOperation(Operation.OP_REMOVE, item, true); return true; } return false; diff --git a/Assets/Mirror/Runtime/SyncSet.cs.meta b/Assets/Mirror/Core/SyncSet.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/SyncSet.cs.meta rename to Assets/Mirror/Core/SyncSet.cs.meta diff --git a/Assets/Mirror/Core/Tools.meta b/Assets/Mirror/Core/Tools.meta new file mode 100644 index 0000000..8319cae --- /dev/null +++ b/Assets/Mirror/Core/Tools.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d9c73cc2749d43268600b80df0b55c4d +timeCreated: 1667486812 \ No newline at end of file diff --git a/Assets/Mirror/Core/Tools/AccurateInterval.cs b/Assets/Mirror/Core/Tools/AccurateInterval.cs new file mode 100644 index 0000000..653c7ba --- /dev/null +++ b/Assets/Mirror/Core/Tools/AccurateInterval.cs @@ -0,0 +1,86 @@ +// accurate interval from Mirror II. +// for sync / send intervals where it matters. +// does not(!) do catch-up. +// +// first, let's understand the problem. +// say we need an interval of 10 Hz, so every 100ms in Update we do: +// if (Time.time >= lastTime + interval) +// { +// lastTime = Time.time; +// ... +// } +// +// this seems fine, but actually Time.time will always be a few ms beyond +// the interval. but since lastTime is reset to Time.time, the remainder +// is always ignored away. +// with fixed tickRate servers (say 30 Hz), the remainder is significant! +// +// in practice if we have a 30 Hz tickRate server with a 30 Hz sendRate, +// the above way to measure the interval would result in a 18-19 Hz sendRate! +// => this is not just a little off. this is _way_ off, by almost half. +// => displaying actual + target tick/send rate will show this very easily. +// +// we need an accurate way to measure intervals for where it matters. +// and it needs to be testable to guarantee results. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class AccurateInterval + { + // static func instead of storing interval + lastTime struct. + // + don't need to initialize struct ctor with interval in Awake + // + allows for interval changes at runtime too + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Elapsed(double time, double interval, ref double lastTime) + { + if (time >= lastTime + interval) + { + // naive implementation: + //lastTime = time; + + // accurate but doesn't handle heavy load situations: + //lastTime += interval; + + // heavy load edge case: + // * interval is 100ms + // * server is under heavy load, Updates slow down to 1/s + // * Elapsed(1.000) returns true. + // technically 10 intervals have elapsed. + // * server recovers to normal, Updates are every 10ms again + // * Elapsed(1.010) should return false again until 1.100. + // + // increasing lastTime by interval would require 10 more calls + // to ever catch up again: + // lastTime += interval + // + // as result, the next 10 calls to Elapsed would return true. + // Elapsed(1.001) => true + // Elapsed(1.002) => true + // Elapsed(1.003) => true + // ... + // even though technically the delta was not >= interval. + // + // this would keep the server under heavy load, and it may never + // catch-up. this is not ideal for large virtual worlds. + // + // instead, we want to skip multiples of 'interval' and only + // keep the remainder. + // + // see also: AccurateIntervalTests.Slowdown() + + // easy to understand: + //double elapsed = time - lastTime; + //double remainder = elapsed % interval; + //lastTime = time - remainder; + + // easier: set to rounded multiples of interval (fholm). + // long to match double time. + long multiplier = (long)(time / interval); + lastTime = multiplier * interval; + return true; + } + return false; + } + } +} diff --git a/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta b/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta new file mode 100644 index 0000000..a58184d --- /dev/null +++ b/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c1b18064e25046f28b88db65a4012ec1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Compression.cs b/Assets/Mirror/Core/Tools/Compression.cs similarity index 76% rename from Assets/Mirror/Runtime/Compression.cs rename to Assets/Mirror/Core/Tools/Compression.cs index 3e4b0f6..2a922fc 100644 --- a/Assets/Mirror/Runtime/Compression.cs +++ b/Assets/Mirror/Core/Tools/Compression.cs @@ -8,6 +8,128 @@ namespace Mirror /// Functions to Compress Quaternions and Floats public static class Compression { + // divide by precision (functions backported from Mirror II) + // for example, 0.1 cm precision converts '5.0f' float to '50' long. + // + // 'long' instead of 'int' to allow for large enough worlds. + // value / precision exceeds int.max range too easily. + // Convert.ToInt32/64 would throw. + // https://github.com/vis2k/DOTSNET/issues/59 + // + // 'long' and 'int' will result in the same bandwidth though. + // for example, ScaleToLong(10.5, 0.1) = 105. + // int: 0x00000069 + // long: 0x0000000000000069 + // delta compression will reduce both to 1 byte. + // + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + public static bool ScaleToLong(float value, float precision, out long result) + { + // user might try to pass precision = 0 to disable rounding. + // this is not supported. + // throw to make the user fix this immediately. + // otherwise we would have to reinterpret-cast if ==0 etc. + // this function should be kept simple. + // if rounding isn't wanted, this function shouldn't be called. + if (precision == 0) throw new DivideByZeroException($"ScaleToLong: precision=0 would cause null division. If rounding isn't wanted, don't call this function."); + + // catch OverflowException if value/precision > long.max. + // attackers should never be able to throw exceptions. + try + { + result = Convert.ToInt64(value / precision); + return true; + } + // clamp to .max/.min. + // returning '0' would make far away entities reset to origin. + // returning 'max' would keep them stuck at the end of the world. + // the latter is much easier to debug. + catch (OverflowException) + { + result = value > 0 ? long.MaxValue : long.MinValue; + return false; + } + } + + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Vector3 value, float precision, out long x, out long y, out long z) + { + // attempt to convert every component. + // do not return early if one conversion returned 'false'. + // the return value is optional. always attempt to convert all. + bool result = true; + result &= ScaleToLong(value.x, precision, out x); + result &= ScaleToLong(value.y, precision, out y); + result &= ScaleToLong(value.z, precision, out z); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long quantized) + { + quantized = Vector3Long.zero; + return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z); + } + + // multiple by precision. + // for example, 0.1 cm precision converts '50' long to '5.0f' float. + public static float ScaleToFloat(long value, float precision) + { + // user might try to pass precision = 0 to disable rounding. + // this is not supported. + // throw to make the user fix this immediately. + // otherwise we would have to reinterpret-cast if ==0 etc. + // this function should be kept simple. + // if rounding isn't wanted, this function shouldn't be called. + if (precision == 0) throw new DivideByZeroException($"ScaleToLong: precision=0 would cause null division. If rounding isn't wanted, don't call this function."); + + return value * precision; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ScaleToFloat(long x, long y, long z, float precision) + { + Vector3 v; + v.x = ScaleToFloat(x, precision); + v.y = ScaleToFloat(y, precision); + v.z = ScaleToFloat(z, precision); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ScaleToFloat(Vector3Long value, float precision) => + ScaleToFloat(value.x, value.y, value.z, precision); + + // scale a float within min/max range to an ushort between min/max range + // note: can also use this for byte range from byte.MinValue to byte.MaxValue + public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget) + { + // note: C# ushort - ushort => int, hence so many casts + // max ushort - min ushort only fits into something bigger + int targetRange = maxTarget - minTarget; + float valueRange = maxValue - minValue; + float valueRelative = value - minValue; + return (ushort)(minTarget + (ushort)(valueRelative / valueRange * targetRange)); + } + + // scale an ushort within min/max range to a float between min/max range + // note: can also use this for byte range from byte.MinValue to byte.MaxValue + public static float ScaleUShortToFloat(ushort value, ushort minValue, ushort maxValue, float minTarget, float maxTarget) + { + // note: C# ushort - ushort => int, hence so many casts + float targetRange = maxTarget - minTarget; + ushort valueRange = (ushort)(maxValue - minValue); + ushort valueRelative = (ushort)(value - minValue); + return minTarget + (valueRelative / (float)valueRange * targetRange); + } + // quaternion compression ////////////////////////////////////////////// // smallest three: https://gafferongames.com/post/snapshot_compression/ // compresses 16 bytes quaternion into 4 bytes @@ -50,29 +172,6 @@ public static int LargestAbsoluteComponentIndex(Vector4 value, out float largest return largestIndex; } - // scale a float within min/max range to an ushort between min/max range - // note: can also use this for byte range from byte.MinValue to byte.MaxValue - public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget) - { - // note: C# ushort - ushort => int, hence so many casts - // max ushort - min ushort only fits into something bigger - int targetRange = maxTarget - minTarget; - float valueRange = maxValue - minValue; - float valueRelative = value - minValue; - return (ushort)(minTarget + (ushort)(valueRelative / valueRange * targetRange)); - } - - // scale an ushort within min/max range to a float between min/max range - // note: can also use this for byte range from byte.MinValue to byte.MaxValue - public static float ScaleUShortToFloat(ushort value, ushort minValue, ushort maxValue, float minTarget, float maxTarget) - { - // note: C# ushort - ushort => int, hence so many casts - float targetRange = maxTarget - minTarget; - ushort valueRange = (ushort)(maxValue - minValue); - ushort valueRelative = (ushort)(value - minValue); - return minTarget + (valueRelative / (float)valueRange * targetRange); - } - const float QuaternionMinRange = -0.707107f; const float QuaternionMaxRange = 0.707107f; const ushort TenBitsMax = 0x3FF; @@ -195,7 +294,7 @@ public static Quaternion DecompressQuaternion(uint data) // varint compression ////////////////////////////////////////////////// // compress ulong varint. - // same result for int, short and byte. only need one function. + // same result for ulong, uint, ushort and byte. only need one function. // NOT an extension. otherwise weaver might accidentally use it. public static void CompressVarUInt(NetworkWriter writer, ulong value) { diff --git a/Assets/Mirror/Runtime/Compression.cs.meta b/Assets/Mirror/Core/Tools/Compression.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Compression.cs.meta rename to Assets/Mirror/Core/Tools/Compression.cs.meta diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs b/Assets/Mirror/Core/Tools/DeltaCompression.cs new file mode 100644 index 0000000..a59370a --- /dev/null +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs @@ -0,0 +1,38 @@ +// manual delta compression for some types. +// varint(b-a) +// Mirror can't use Mirror II's bit-tree delta compression. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class DeltaCompression + { + // delta (usually small), then zigzag varint to support +- changes + // parameter order: (last, current) makes most sense (Q3 does this too). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, long last, long current) => + Compression.CompressVarInt(writer, current - last); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Decompress(NetworkReader reader, long last) => + last + Compression.DecompressVarInt(reader); + + // delta (usually small), then zigzag varint to support +- changes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, Vector3Long last, Vector3Long current) + { + Compress(writer, last.x, current.x); + Compress(writer, last.y, current.y); + Compress(writer, last.z, current.z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long Decompress(NetworkReader reader, Vector3Long last) + { + long x = Decompress(reader, last.x); + long y = Decompress(reader, last.y); + long z = Decompress(reader, last.z); + return new Vector3Long(x, y, z); + } + } +} diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta b/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta new file mode 100644 index 0000000..a7b2c8b --- /dev/null +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b8f3fffcb4754c15bc5ed4c33e2497b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs similarity index 54% rename from Assets/Mirror/Runtime/ExponentialMovingAverage.cs rename to Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs index 64b91e1..2080bc7 100644 --- a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs +++ b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs @@ -1,20 +1,27 @@ +// N-day EMA implementation from Mirror with a few changes (struct etc.) +// it calculates an exponential moving average roughly equivalent to the last n observations +// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average +using System; + namespace Mirror { - // implementation of N-day EMA - // it calculates an exponential moving average roughly equivalent to the last n observations - // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average - public class ExponentialMovingAverage + public struct ExponentialMovingAverage { readonly float alpha; bool initialized; public double Value; - public double Var; + public double Variance; + public double StandardDeviation; // absolute value, see test public ExponentialMovingAverage(int n) { // standard N-day EMA alpha calculation alpha = 2.0f / (n + 1); + initialized = false; + Value = 0; + Variance = 0; + StandardDeviation = 0; } public void Add(double newValue) @@ -25,7 +32,8 @@ public void Add(double newValue) { double delta = newValue - Value; Value += alpha * delta; - Var = (1 - alpha) * (Var + alpha * delta * delta); + Variance = (1 - alpha) * (Variance + alpha * delta * delta); + StandardDeviation = Math.Sqrt(Variance); } else { diff --git a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta rename to Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta diff --git a/Assets/Mirror/Runtime/Extensions.cs b/Assets/Mirror/Core/Tools/Extensions.cs similarity index 86% rename from Assets/Mirror/Runtime/Extensions.cs rename to Assets/Mirror/Core/Tools/Extensions.cs index 3d285e9..3654e4b 100644 --- a/Assets/Mirror/Runtime/Extensions.cs +++ b/Assets/Mirror/Core/Tools/Extensions.cs @@ -6,6 +6,9 @@ namespace Mirror { public static class Extensions { + public static string ToHexString(this ArraySegment segment) => + BitConverter.ToString(segment.Array, segment.Offset, segment.Count); + // string.GetHashCode is not guaranteed to be the same on all machines, but // we need one that is the same on all machines. simple and stupid: public static int GetStableHashCode(this string text) @@ -40,7 +43,7 @@ public static void CopyTo(this IEnumerable source, List destination) } #if !UNITY_2021_OR_NEWER - // Unity 2019 / 2020 don't have Queue.TryDeque which we need for batching. + // Unity 2020 and earlier doesn't have Queue.TryDequeue which we need for batching. public static bool TryDequeue(this Queue source, out T element) { if (source.Count > 0) diff --git a/Assets/Mirror/Runtime/Extensions.cs.meta b/Assets/Mirror/Core/Tools/Extensions.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Extensions.cs.meta rename to Assets/Mirror/Core/Tools/Extensions.cs.meta diff --git a/Assets/Mirror/Runtime/Mathd.cs b/Assets/Mirror/Core/Tools/Mathd.cs similarity index 99% rename from Assets/Mirror/Runtime/Mathd.cs rename to Assets/Mirror/Core/Tools/Mathd.cs index 2dfa2f9..af5d7b8 100644 --- a/Assets/Mirror/Runtime/Mathd.cs +++ b/Assets/Mirror/Core/Tools/Mathd.cs @@ -1,16 +1,10 @@ // 'double' precision variants for some of Unity's Mathf functions. - using System.Runtime.CompilerServices; namespace Mirror { public static class Mathd { - /// Linearly interpolates between a and b by t with no limit to t. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double LerpUnclamped(double a, double b, double t) => - a + (b - a) * t; - /// Clamps value between 0 and 1 and returns value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Clamp01(double value) @@ -24,5 +18,10 @@ public static double Clamp01(double value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double InverseLerp(double a, double b, double value) => a != b ? Clamp01((value - a) / (b - a)) : 0; + + /// Linearly interpolates between a and b by t with no limit to t. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double LerpUnclamped(double a, double b, double t) => + a + (b - a) * t; } } diff --git a/Assets/Mirror/Runtime/Mathd.cs.meta b/Assets/Mirror/Core/Tools/Mathd.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Mathd.cs.meta rename to Assets/Mirror/Core/Tools/Mathd.cs.meta diff --git a/Assets/Mirror/Runtime/Pool.cs b/Assets/Mirror/Core/Tools/Pool.cs similarity index 91% rename from Assets/Mirror/Runtime/Pool.cs rename to Assets/Mirror/Core/Tools/Pool.cs index e526139..e204575 100644 --- a/Assets/Mirror/Runtime/Pool.cs +++ b/Assets/Mirror/Core/Tools/Pool.cs @@ -8,7 +8,8 @@ namespace Mirror { public class Pool { - // Mirror is single threaded, no need for concurrent collections + // Mirror is single threaded, no need for concurrent collections. + // stack increases the chance that a reused writer remains in cache. readonly Stack objects = new Stack(); // some types might need additional parameters in their constructor, so @@ -25,10 +26,6 @@ public Pool(Func objectGenerator, int initialCapacity) objects.Push(objectGenerator()); } - // DEPRECATED 2022-03-10 - [Obsolete("Take() was renamed to Get()")] - public T Take() => Get(); - // take an element from the pool, or create a new one if empty [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Get() => objects.Count > 0 ? objects.Pop() : objectGenerator(); diff --git a/Assets/Mirror/Runtime/Pool.cs.meta b/Assets/Mirror/Core/Tools/Pool.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Pool.cs.meta rename to Assets/Mirror/Core/Tools/Pool.cs.meta diff --git a/Assets/Mirror/Core/Tools/Readme.txt b/Assets/Mirror/Core/Tools/Readme.txt new file mode 100644 index 0000000..09bd920 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Readme.txt @@ -0,0 +1 @@ +Standalone algorithms & structs to help build Mirror. \ No newline at end of file diff --git a/Assets/Mirror/Core/Tools/Readme.txt.meta b/Assets/Mirror/Core/Tools/Readme.txt.meta new file mode 100644 index 0000000..5536ca7 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Readme.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: da033671de7d49e0838223a997c56bf1 +timeCreated: 1667486850 \ No newline at end of file diff --git a/Assets/Mirror/Core/Tools/TimeSample.cs b/Assets/Mirror/Core/Tools/TimeSample.cs new file mode 100644 index 0000000..111e971 --- /dev/null +++ b/Assets/Mirror/Core/Tools/TimeSample.cs @@ -0,0 +1,61 @@ +// TimeSample from Mirror II. +// simple profiling sample, averaged for display in statistics. +// usable in builds without unitiy profiler overhead etc. +// +// .average may safely be called from main thread while Begin/End is in another. +// i.e. worker threads, transport, etc. +using System.Diagnostics; +using System.Threading; + +namespace Mirror +{ + public struct TimeSample + { + // UnityEngine.Time isn't thread safe. use stopwatch instead. + readonly Stopwatch watch; + + // remember when Begin was called + double beginTime; + + // keep accumulating times over the given interval. + // (not readonly. we modify its contents.) + ExponentialMovingAverage ema; + + // average in seconds. + // code often runs in sub-millisecond time. float is more precise. + // + // set with Interlocked for thread safety. + // can be read from main thread while sampling happens in other thread. + public double average; // THREAD SAFE + + // average over N begin/end captures + public TimeSample(int n) + { + watch = new Stopwatch(); + watch.Start(); + ema = new ExponentialMovingAverage(n); + beginTime = 0; + average = 0; + } + + // begin is called before the code to be sampled + public void Begin() + { + // remember when Begin was called. + // keep StopWatch running so we can average over the given interval. + beginTime = watch.Elapsed.TotalSeconds; + // Debug.Log($"Begin @ {beginTime:F4}"); + } + + // end is called after the code to be sampled + public void End() + { + // add duration in seconds to accumulated durations + double elapsed = watch.Elapsed.TotalSeconds - beginTime; + ema.Add(elapsed); + + // expose new average thread safely + Interlocked.Exchange(ref average, ema.Value); + } + } +} diff --git a/Assets/Mirror/Core/Tools/TimeSample.cs.meta b/Assets/Mirror/Core/Tools/TimeSample.cs.meta new file mode 100644 index 0000000..83e5ede --- /dev/null +++ b/Assets/Mirror/Core/Tools/TimeSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26c32f6429554546a88d800c846c74ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Utils.cs b/Assets/Mirror/Core/Tools/Utils.cs similarity index 56% rename from Assets/Mirror/Runtime/Utils.cs rename to Assets/Mirror/Core/Tools/Utils.cs index d39ed98..bf189e4 100644 --- a/Assets/Mirror/Runtime/Utils.cs +++ b/Assets/Mirror/Core/Tools/Utils.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Security.Cryptography; using UnityEngine; +using UnityEngine.SceneManagement; namespace Mirror { @@ -9,7 +10,7 @@ namespace Mirror public delegate void NetworkMessageDelegate(NetworkConnection conn, NetworkReader reader, int channelId); // Handles requests to spawn objects on the client - public delegate GameObject SpawnDelegate(Vector3 position, Guid assetId); + public delegate GameObject SpawnDelegate(Vector3 position, uint assetId); public delegate GameObject SpawnHandlerDelegate(SpawnMessage msg); @@ -51,6 +52,22 @@ public static bool IsPrefab(GameObject obj) #endif } + // simplified IsSceneObject check from Mirror II + public static bool IsSceneObject(NetworkIdentity identity) + { + // original UNET / Mirror still had the IsPersistent check. + // it never fires though. even for Prefabs dragged to the Scene. + // (see Scene Objects example scene.) + // #if UNITY_EDITOR + // if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) + // return false; + // #endif + + return identity.gameObject.hideFlags != HideFlags.NotEditable && + identity.gameObject.hideFlags != HideFlags.HideAndDontSave && + identity.sceneId != 0; + } + public static bool IsSceneObjectWithPrefabParent(GameObject gameObject, out GameObject prefab) { prefab = null; @@ -96,6 +113,23 @@ public static string PrettyBytes(long bytes) return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB"; } + // pretty print seconds as hours:minutes:seconds(.milliseconds/100)s. + // double for long running servers. + public static string PrettySeconds(double seconds) + { + TimeSpan t = TimeSpan.FromSeconds(seconds); + string res = ""; + if (t.Days > 0) res += $"{t.Days}d"; + if (t.Hours > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Hours}h"; + if (t.Minutes > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Minutes}m"; + // 0.5s, 1.5s etc. if any milliseconds. 1s, 2s etc. if any seconds + if (t.Milliseconds > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Seconds}.{(t.Milliseconds / 100)}s"; + else if (t.Seconds > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Seconds}s"; + // if the string is still empty because the value was '0', then at least + // return the seconds instead of returning an empty string + return res != "" ? res : "0s"; + } + // universal .spawned function [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkIdentity GetSpawnedInServerOrClient(uint netId) @@ -117,5 +151,39 @@ public static NetworkIdentity GetSpawnedInServerOrClient(uint netId) return null; } + + // keep a GUI window in screen. + // for example. if it's at x=1000 and screen is resized to w=500, + // it won't get lost in the invisible area etc. + public static Rect KeepInScreen(Rect rect) + { + // ensure min + rect.x = Math.Max(rect.x, 0); + rect.y = Math.Max(rect.y, 0); + + // ensure max + rect.x = Math.Min(rect.x, Screen.width - rect.width); + rect.y = Math.Min(rect.y, Screen.width - rect.height); + + return rect; + } + + // create local connections pair and connect them + public static void CreateLocalConnections( + out LocalConnectionToClient connectionToClient, + out LocalConnectionToServer connectionToServer) + { + connectionToServer = new LocalConnectionToServer(); + connectionToClient = new LocalConnectionToClient(); + connectionToServer.connectionToClient = connectionToClient; + connectionToClient.connectionToServer = connectionToServer; + } + + public static bool IsSceneActive(string scene) + { + Scene activeScene = SceneManager.GetActiveScene(); + return activeScene.path == scene || + activeScene.name == scene; + } } } diff --git a/Assets/Mirror/Runtime/Utils.cs.meta b/Assets/Mirror/Core/Tools/Utils.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Utils.cs.meta rename to Assets/Mirror/Core/Tools/Utils.cs.meta diff --git a/Assets/Mirror/Core/Tools/Vector3Long.cs b/Assets/Mirror/Core/Tools/Vector3Long.cs new file mode 100644 index 0000000..d54a46c --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector3Long.cs @@ -0,0 +1,125 @@ +#pragma warning disable CS0659 // 'Vector3Long' overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // 'Vector3Long' defines operator == or operator != but does not override Object.GetHashCode() + +// Vector3Long by mischa (based on game engine project) +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public struct Vector3Long + { + public long x; + public long y; + public long z; + + public static readonly Vector3Long zero = new Vector3Long(0, 0, 0); + public static readonly Vector3Long one = new Vector3Long(1, 1, 1); + public static readonly Vector3Long forward = new Vector3Long(0, 0, 1); + public static readonly Vector3Long back = new Vector3Long(0, 0, -1); + public static readonly Vector3Long left = new Vector3Long(-1, 0, 0); + public static readonly Vector3Long right = new Vector3Long(1, 0, 0); + public static readonly Vector3Long up = new Vector3Long(0, 1, 0); + public static readonly Vector3Long down = new Vector3Long(0, -1, 0); + + // constructor ///////////////////////////////////////////////////////// + public Vector3Long(long x, long y, long z) + { + this.x = x; + this.y = y; + this.z = z; + } + + // operators /////////////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator +(Vector3Long a, Vector3Long b) => + new Vector3Long(a.x + b.x, a.y + b.y, a.z + b.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator -(Vector3Long a, Vector3Long b) => + new Vector3Long(a.x - b.x, a.y - b.y, a.z - b.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator -(Vector3Long v) => + new Vector3Long(-v.x, -v.y, -v.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator *(Vector3Long a, long n) => + new Vector3Long(a.x * n, a.y * n, a.z * n); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator *(long n, Vector3Long a) => + new Vector3Long(a.x * n, a.y * n, a.z * n); + + // == returns true if approximately equal (with epsilon). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3Long a, Vector3Long b) => + a.x == b.x && + a.y == b.y && + a.z == b.z; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector3Long a, Vector3Long b) => !(a == b); + + // NO IMPLICIT System.Numerics.Vector3Long conversion because double<->float + // would silently lose precision in large worlds. + + // [i] component index. useful for iterating all components etc. + public long this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + default: throw new IndexOutOfRangeException($"Vector3Long[{index}] out of range."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: throw new IndexOutOfRangeException($"Vector3Long[{index}] out of range."); + } + } + } + + // instance functions ////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"({x} {y} {z})"; + + // equality //////////////////////////////////////////////////////////// + // implement Equals & HashCode explicitly for performance. + // calling .Equals (instead of "==") checks for exact equality. + // (API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Vector3Long other) => + x == other.x && y == other.y && z == other.z; + + // Equals(object) can reuse Equals(Vector4) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object other) => + other is Vector3Long vector4 && Equals(vector4); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(x, y, z); +#endif + } +} diff --git a/Assets/Mirror/Core/Tools/Vector3Long.cs.meta b/Assets/Mirror/Core/Tools/Vector3Long.cs.meta new file mode 100644 index 0000000..2239765 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector3Long.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18efa4e349254185ad257401dd24628b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transport.cs b/Assets/Mirror/Core/Transport.cs similarity index 94% rename from Assets/Mirror/Runtime/Transport.cs rename to Assets/Mirror/Core/Transport.cs index 13f2b9f..7e0716d 100644 --- a/Assets/Mirror/Runtime/Transport.cs +++ b/Assets/Mirror/Core/Transport.cs @@ -31,11 +31,12 @@ namespace Mirror public abstract class Transport : MonoBehaviour { /// The current transport used by Mirror. - public static Transport activeTransport; + public static Transport active; /// Is this transport available in the current platform? public abstract bool Available(); + // client ////////////////////////////////////////////////////////////// /// Called by Transport when the client connected to the server. public Action OnClientConnected; @@ -50,11 +51,33 @@ public abstract class Transport : MonoBehaviour public Action, int> OnClientDataSent; /// Called by Transport when the client encountered an error. - public Action OnClientError; + public Action OnClientError; /// Called by Transport when the client disconnected from the server. public Action OnClientDisconnected; + // server ////////////////////////////////////////////////////////////// + /// Called by Transport when a new client connected to the server. + public Action OnServerConnected; + + /// Called by Transport when the server received a message from a client. + public Action, int> OnServerDataReceived; + + /// Called by Transport when the server sent a message to a client. + // Transports are responsible for calling it because: + // - groups it together with OnReceived responsibility + // - allows transports to decide if anything was sent or not + // - allows transports to decide the actual used channel (i.e. tcp always sending reliable) + public Action, int> OnServerDataSent; + + /// Called by Transport when a server's connection encountered a problem. + /// If a Disconnect will also be raised, raise the Error first. + public Action OnServerError; + + /// Called by Transport when a client disconnected from the server. + public Action OnServerDisconnected; + + // client functions //////////////////////////////////////////////////// /// True if the client is currently connected to the server. public abstract bool ClientConnected(); @@ -76,30 +99,11 @@ public virtual void ClientConnect(Uri uri) /// Disconnects the client from the server public abstract void ClientDisconnect(); + // server functions //////////////////////////////////////////////////// /// Returns server address as Uri. // Useful for NetworkDiscovery. public abstract Uri ServerUri(); - /// Called by Transport when a new client connected to the server. - public Action OnServerConnected; - - /// Called by Transport when the server received a message from a client. - public Action, int> OnServerDataReceived; - - /// Called by Transport when the server sent a message to a client. - // Transports are responsible for calling it because: - // - groups it together with OnReceived responsibility - // - allows transports to decide if anything was sent or not - // - allows transports to decide the actual used channel (i.e. tcp always sending reliable) - public Action, int> OnServerDataSent; - - /// Called by Transport when a server's connection encountered a problem. - /// If a Disconnect will also be raised, raise the Error first. - public Action OnServerError; - - /// Called by Transport when a client disconnected from the server. - public Action OnServerDisconnected; - /// True if the server is currently listening for connections. public abstract bool ServerActive(); diff --git a/Assets/Mirror/Runtime/Transport.cs.meta b/Assets/Mirror/Core/Transport.cs.meta similarity index 100% rename from Assets/Mirror/Runtime/Transport.cs.meta rename to Assets/Mirror/Core/Transport.cs.meta diff --git a/Assets/Mirror/Core/TransportError.cs b/Assets/Mirror/Core/TransportError.cs new file mode 100644 index 0000000..b452015 --- /dev/null +++ b/Assets/Mirror/Core/TransportError.cs @@ -0,0 +1,17 @@ +// Mirror transport error code enum. +// most transport implementations should use a subset of this, +// and then translate the transport error codes to mirror error codes. +namespace Mirror +{ + public enum TransportError : byte + { + DnsResolve, // failed to resolve a host name + Refused, // connection refused by other end. server full etc. + Timeout, // ping timeout or dead link + Congestion, // more messages than transport / network can process + InvalidReceive, // recv invalid packet (possibly intentional attack) + InvalidSend, // user tried to send invalid data + ConnectionClosed, // connection closed voluntarily or lost involuntarily + Unexpected // unexpected error / exception, requires fix. + } +} diff --git a/Assets/Mirror/Core/TransportError.cs.meta b/Assets/Mirror/Core/TransportError.cs.meta new file mode 100644 index 0000000..d1d0fa0 --- /dev/null +++ b/Assets/Mirror/Core/TransportError.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce162bdedd704db9b8c35d163f0c1d54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/AndroidManifestHelper.cs b/Assets/Mirror/Editor/AndroidManifestHelper.cs index 78f408d..c76359a 100644 --- a/Assets/Mirror/Editor/AndroidManifestHelper.cs +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs @@ -24,7 +24,7 @@ public void OnPostGenerateGradleAndroidProject(string path) { string manifestFolder = Path.Combine(path, "src/main"); string sourceFile = manifestFolder + "/AndroidManifest.xml"; - // Load android manfiest file + // Load android manifest file XmlDocument doc = new XmlDocument(); doc.Load(sourceFile); @@ -61,7 +61,7 @@ public void OnPostGenerateGradleAndroidProject(string path) } #endif - static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs + static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs { var nodes = doc.SelectNodes(path + "/" + elementName); XmlElement element = null; diff --git a/Assets/Mirror/Editor/Empty/SyncVarDrawer.cs b/Assets/Mirror/Editor/Empty/SyncVarDrawer.cs new file mode 100644 index 0000000..aaa3b9d --- /dev/null +++ b/Assets/Mirror/Editor/Empty/SyncVarDrawer.cs @@ -0,0 +1 @@ +// removed 2022-11-03 diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs.meta b/Assets/Mirror/Editor/Empty/SyncVarDrawer.cs.meta similarity index 100% rename from Assets/Mirror/Editor/SyncVarDrawer.cs.meta rename to Assets/Mirror/Editor/Empty/SyncVarDrawer.cs.meta diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef b/Assets/Mirror/Editor/Mirror.Editor.asmdef index 0d59f9f..651b8fe 100644 --- a/Assets/Mirror/Editor/Mirror.Editor.asmdef +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef @@ -2,8 +2,8 @@ "name": "Mirror.Editor", "rootNamespace": "", "references": [ - "Mirror", - "Unity.Mirror.CodeGen" + "GUID:30817c1a0e6d646d99c048fc403f5979", + "GUID:1d0b9d21c3ff546a4aa32399dfd33474" ], "includePlatforms": [ "Editor" diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index 54b3ae7..7cae54f 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -85,7 +85,15 @@ protected void DrawDefaultSyncSettings() EditorGUILayout.Space(); EditorGUILayout.LabelField("Sync Settings", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + // sync direction + SerializedProperty syncDirection = serializedObject.FindProperty("syncDirection"); + EditorGUILayout.PropertyField(syncDirection); + + // sync mdoe: only show for ServerToClient components + if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient) + EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + + // sync interval EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval")); // apply diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs b/Assets/Mirror/Editor/NetworkInformationPreview.cs index 2c8874b..ec0a01e 100644 --- a/Assets/Mirror/Editor/NetworkInformationPreview.cs +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs @@ -171,7 +171,7 @@ float DrawNetworkBehaviors(NetworkIdentity identity, float initialX, float Y) float DrawObservers(NetworkIdentity identity, float initialX, float Y) { - if (identity.observers != null && identity.observers.Count > 0) + if (identity.observers.Count > 0) { Rect observerRect = new Rect(initialX, Y + 10, 200, 20); @@ -252,7 +252,7 @@ IEnumerable GetNetworkIdentityInfo(NetworkIdentity identity infos.Add(GetString("Network ID", identity.netId.ToString())); infos.Add(GetBoolean("Is Client", identity.isClient)); infos.Add(GetBoolean("Is Server", identity.isServer)); - infos.Add(GetBoolean("Has Authority", identity.hasAuthority)); + infos.Add(GetBoolean("Is Owned", identity.isOwned)); infos.Add(GetBoolean("Is Local Player", identity.isLocalPlayer)); } return infos; diff --git a/Assets/Mirror/Editor/NetworkScenePostProcess.cs b/Assets/Mirror/Editor/NetworkScenePostProcess.cs index c60493d..6dcd65d 100644 --- a/Assets/Mirror/Editor/NetworkScenePostProcess.cs +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs @@ -80,7 +80,9 @@ static void PrepareSceneObject(NetworkIdentity identity) // set scene hash identity.SetSceneIdSceneHashPartInternal(); - // disable it + // spawnable scene objects are force disabled on scene load to + // ensure Start/Update/etc. aren't called until actually spawned. + // // note: NetworkIdentity.OnDisable adds itself to the // spawnableObjects dictionary (only if sceneId != 0) identity.gameObject.SetActive(false); diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs b/Assets/Mirror/Editor/SyncVarDrawer.cs deleted file mode 100644 index b0532ae..0000000 --- a/Assets/Mirror/Editor/SyncVarDrawer.cs +++ /dev/null @@ -1,35 +0,0 @@ -// SyncVar looks like this in the Inspector: -// Health -// Value: 42 -// instead, let's draw ._Value directly so it looks like this: -// Health: 42 -// -// BUG: Unity also doesn't show custom drawer for readonly fields (#1368395) -using UnityEditor; -using UnityEngine; - -namespace Mirror -{ - [CustomPropertyDrawer(typeof(SyncVar<>))] - public class SyncVarDrawer : PropertyDrawer - { - static readonly GUIContent syncVarIndicatorContent = new GUIContent("SyncVar", "This variable is a SyncVar."); - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - Vector2 syncVarIndicatorRect = EditorStyles.miniLabel.CalcSize(syncVarIndicatorContent); - float valueWidth = position.width - syncVarIndicatorRect.x; - - Rect valueRect = new Rect(position.x, position.y, valueWidth, position.height); - Rect labelRect = new Rect(position.x + valueWidth, position.y, syncVarIndicatorRect.x, position.height); - - EditorGUI.PropertyField(valueRect, property.FindPropertyRelative("_Value"), label, true); - GUI.Label(labelRect, syncVarIndicatorContent, EditorStyles.miniLabel); - } - - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) - { - return EditorGUI.GetPropertyHeight(property.FindPropertyRelative("_Value")); - } - } -} diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs b/Assets/Mirror/Editor/Weaver/Extensions.cs index e5ddb1f..9a5759a 100644 --- a/Assets/Mirror/Editor/Weaver/Extensions.cs +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs @@ -12,8 +12,18 @@ public static bool Is(this TypeReference td, Type type) => ? td.GetElementType().FullName == type.FullName : td.FullName == type.FullName; + // check if 'td' is exactly of type T. + // it does not check if any base type is of , only the specific type. + // for example: + // NetworkConnection Is NetworkConnection: true + // NetworkConnectionToClient Is NetworkConnection: false public static bool Is(this TypeReference td) => Is(td, typeof(T)); + // check if 'tr' is derived from T. + // it does not check if 'tr' is exactly T. + // for example: + // NetworkConnection IsDerivedFrom: false + // NetworkConnectionToClient IsDerivedFrom: true public static bool IsDerivedFrom(this TypeReference tr) => IsDerivedFrom(tr, typeof(T)); public static bool IsDerivedFrom(this TypeReference tr, Type baseClass) @@ -266,7 +276,7 @@ public static AssemblyNameReference FindReference(this ModuleDefinition module, // Takes generic arguments from child class and applies them to parent reference, if possible // eg makes `Base` in Child : Base have `int` instead of `T` - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 public static TypeReference ApplyGenericParameters(this TypeReference parentReference, TypeReference childReference) @@ -306,7 +316,7 @@ public static TypeReference ApplyGenericParameters(this TypeReference parentRefe } // Finds the type reference for a generic parameter with the provided name in the child reference - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 static TypeReference FindMatchingGenericArgument(TypeReference childReference, string paramName) { diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs index ac00f65..7c1b2c3 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -397,7 +397,7 @@ void GenerateSerialization(ref bool WeavingFailed) MethodDefinition serialize = new MethodDefinition(SerializeMethodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - weaverTypes.Import()); + weaverTypes.Import(typeof(void))); serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import())); serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, weaverTypes.Import())); @@ -405,10 +405,7 @@ void GenerateSerialization(ref bool WeavingFailed) serialize.Body.InitLocals = true; - // loc_0, this local variable is to determine if any variable was dirty - VariableDefinition dirtyLocal = new VariableDefinition(weaverTypes.Import()); - serialize.Body.Variables.Add(dirtyLocal); - + // base.SerializeSyncVars(writer, forceAll); MethodReference baseSerialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, SerializeMethodName); if (baseSerialize != null) { @@ -419,16 +416,20 @@ void GenerateSerialization(ref bool WeavingFailed) // forceAll worker.Emit(OpCodes.Ldarg_2); worker.Emit(OpCodes.Call, baseSerialize); - // set dirtyLocal to result of base.OnSerialize() - worker.Emit(OpCodes.Stloc_0); } - // Generates: if (forceAll); + // Generates: + // if (forceAll) + // { + // writer.WriteInt(health); + // ... + // } Instruction initialStateLabel = worker.Create(OpCodes.Nop); // forceAll - worker.Emit(OpCodes.Ldarg_2); - worker.Emit(OpCodes.Brfalse, initialStateLabel); + worker.Emit(OpCodes.Ldarg_2); // load 'forceAll' flag + worker.Emit(OpCodes.Brfalse, initialStateLabel); // start the 'if forceAll' branch + // generates write.Write(syncVar) for each SyncVar in forceAll case foreach (FieldDefinition syncVarDef in syncVars) { FieldReference syncVar = syncVarDef; @@ -455,15 +456,14 @@ void GenerateSerialization(ref bool WeavingFailed) } } - // always return true if forceAll - - // Generates: return true - worker.Emit(OpCodes.Ldc_I4_1); + // if (forceAll) then always return at the end of the 'if' case worker.Emit(OpCodes.Ret); - // Generates: end if (forceAll); + // end the 'if' case for "if (forceAll)" worker.Append(initialStateLabel); + //////////////////////////////////////////////////////////////////// + // write dirty bits before the data fields // Generates: writer.WritePackedUInt64 (base.get_syncVarDirtyBits ()); // writer @@ -480,7 +480,6 @@ void GenerateSerialization(ref bool WeavingFailed) int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); foreach (FieldDefinition syncVarDef in syncVars) { - FieldReference syncVar = syncVarDef; if (netBehaviourSubclass.HasGenericParameters) { @@ -516,11 +515,6 @@ void GenerateSerialization(ref bool WeavingFailed) return; } - // something was dirty - worker.Emit(OpCodes.Ldc_I4_1); - // set dirtyLocal to true - worker.Emit(OpCodes.Stloc_0); - worker.Append(varLabel); dirtyBit += 1; } @@ -529,8 +523,7 @@ void GenerateSerialization(ref bool WeavingFailed) //worker.Emit(OpCodes.Ldstr, $"Injected Serialize {netBehaviourSubclass.Name}"); //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); - // generate: return dirtyLocal - worker.Emit(OpCodes.Ldloc_0); + // generate: return worker.Emit(OpCodes.Ret); netBehaviourSubclass.Methods.Add(serialize); } diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs index 2cec8a4..8143e86 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs @@ -16,7 +16,7 @@ public static List FindSyncObjectsFields(Writers writers, Reade foreach (FieldDefinition fd in td.Fields) { - if (fd.FieldType.IsGenericParameter) + if (fd.FieldType.IsGenericParameter || fd.ContainsGenericParameter) { // can't call .Resolve on generic ones continue; diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs index a5f95cd..ee33de3 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs @@ -372,7 +372,7 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary()); + weaverTypes.Import()); netIdField.DeclaringType = td; syncVarNetIds[fd] = netIdField; diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs index 8afba94..757aa44 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs @@ -9,8 +9,16 @@ public static class TargetRpcProcessor // helper functions to check if the method has a NetworkConnection parameter public static bool HasNetworkConnectionParameter(MethodDefinition md) { - return md.Parameters.Count > 0 && - md.Parameters[0].ParameterType.Is(); + if (md.Parameters.Count > 0) + { + // we need to allow both NetworkConnection, and inheriting types. + // NetworkBehaviour.SendTargetRpc takes a NetworkConnection parameter. + // fixes https://github.com/vis2k/Mirror/issues/3290 + TypeReference type = md.Parameters[0].ParameterType; + return type.Is() || + type.IsDerivedFrom(); + } + return false; } public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed) diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef index 4566bb2..987382c 100644 --- a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef @@ -2,7 +2,7 @@ "name": "Unity.Mirror.CodeGen", "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], "includePlatforms": [ "Editor" diff --git a/Assets/Mirror/Editor/Welcome.cs b/Assets/Mirror/Editor/Welcome.cs new file mode 100644 index 0000000..14e57bc --- /dev/null +++ b/Assets/Mirror/Editor/Welcome.cs @@ -0,0 +1,23 @@ +// Shows either a welcome message, only once per session. +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + static class Welcome + { + [InitializeOnLoadMethod] + static void OnInitializeOnLoad() + { + // InitializeOnLoad is called on start and after each rebuild, + // but we only want to show this once per editor session. + if (!SessionState.GetBool("MIRROR_WELCOME", false)) + { + SessionState.SetBool("MIRROR_WELCOME", true); + Debug.Log("Mirror | mirror-networking.com | discord.gg/N9QVxbM"); + } + } + } +} +#endif diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta b/Assets/Mirror/Editor/Welcome.cs.meta similarity index 83% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta rename to Assets/Mirror/Editor/Welcome.cs.meta index c4fa54d..9062df0 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta +++ b/Assets/Mirror/Editor/Welcome.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9e801942544d44d65808fb250623fe25 +guid: 180619c3887314f56bf396769c0a23ee MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Cube.prefab b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Cube.prefab index a250054..45456a0 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Cube.prefab +++ b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Cube.prefab @@ -31,6 +31,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -54,10 +55,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -82,6 +85,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!65 &3181899219042528416 BoxCollider: m_ObjectHideFlags: 0 @@ -108,9 +112,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 2131474587 serverOnly: 0 visible: 0 - m_AssetId: 7c6a0a278eba11e44b9a86cd4da603df hasSpawned: 0 --- !u!114 &96969086124788804 MonoBehaviour: @@ -124,6 +128,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a91a718a70d01b347b75cb768a6f1a92, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: diff --git a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Plane.prefab b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Plane.prefab index 9270e1f..73af6db 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Plane.prefab +++ b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Plane.prefab @@ -33,6 +33,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 50, y: 1, z: 50} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -56,10 +57,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -84,6 +87,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!64 &7349408984269008053 MeshCollider: m_ObjectHideFlags: 0 diff --git a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Player.prefab b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Player.prefab index 340e315..e7a8354 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Player.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5} m_LocalScale: {x: 0.5, y: 0.1, z: 0.2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 962190737825349125} m_RootOrder: 0 @@ -51,10 +52,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -79,6 +82,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &5620029719931856626 GameObject: m_ObjectHideFlags: 0 @@ -107,6 +111,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7513987664611104733} m_Father: {fileID: 464867598898769114} @@ -131,10 +136,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -159,6 +166,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &7619140271685878370 GameObject: m_ObjectHideFlags: 0 @@ -176,7 +184,7 @@ GameObject: - component: {fileID: -2422551836510166228} - component: {fileID: 1763152038191860132} - component: {fileID: 1747637013460943425} - m_Layer: 8 + m_Layer: 0 m_Name: Player m_TagString: Player m_Icon: {fileID: 0} @@ -193,6 +201,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1.08, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 962190737825349125} m_Father: {fileID: 0} @@ -211,9 +220,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 1702147074 serverOnly: 0 visible: 0 - m_AssetId: 9f0094c1325091d42a558274b947221f hasSpawned: 0 --- !u!114 &-4914236621144254103 MonoBehaviour: @@ -227,28 +236,22 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 1 syncMode: 0 - syncInterval: 0.1 - clientAuthority: 1 - sendInterval: 0.05 + syncInterval: 0 + target: {fileID: 464867598898769114} + clientAuthority: 0 syncPosition: 1 syncRotation: 1 syncScale: 0 - interpolatePosition: 1 - interpolateRotation: 1 - interpolateScale: 0 - bufferTimeMultiplier: 1 - bufferSizeLimit: 64 - catchupThreshold: 4 - catchupMultiplier: 0.1 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChange: 1 bufferResetMultiplier: 5 positionSensitivity: 0.01 rotationSensitivity: 0.01 scaleSensitivity: 0.01 - showGizmos: 0 - showOverlay: 0 - overlayColor: {r: 0, g: 0, b: 0, a: 0.5} --- !u!114 &-903079073849018483 MonoBehaviour: m_ObjectHideFlags: 0 @@ -261,6 +264,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 22976424f775a0f4a8531e6713ff6de2, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 --- !u!114 &-1734969889957956087 @@ -270,24 +274,31 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 7619140271685878370} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 05e10150710dde14b83d3c8f5aa853c2, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 characterController: {fileID: 5462452979215896872} - moveSpeed: 8 - turnSensitivity: 5 + controlsCanvas: {fileID: 0} + moveSpeedMultiplier: 8 maxTurnSpeed: 100 + turnDelta: 3 + initialJumpSpeed: 0.2 + maxJumpSpeed: 5 + jumpDelta: 0.2 + groundState: 2 horizontal: 0 vertical: 0 - turn: 0 + turnSpeed: 0 jumpSpeed: 0 - isGrounded: 1 - isFalling: 0 + animVelocity: 0 + animRotation: 0 velocity: {x: 0, y: 0, z: 0} + direction: {x: 0, y: 0, z: 0} --- !u!143 &5462452979215896872 CharacterController: m_ObjectHideFlags: 0 @@ -303,8 +314,8 @@ CharacterController: m_Radius: 0.5 m_SlopeLimit: 45 m_StepOffset: 0.3 - m_SkinWidth: 0.08 - m_MinMoveDistance: 0.001 + m_SkinWidth: 0.02 + m_MinMoveDistance: 0 m_Center: {x: 0, y: 0, z: 0} --- !u!136 &-2422551836510166228 CapsuleCollider: @@ -348,6 +359,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a91a718a70d01b347b75cb768a6f1a92, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: diff --git a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Portal.prefab b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Portal.prefab index 48e2419..c94867a 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Portal.prefab +++ b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Portal.prefab @@ -14,7 +14,7 @@ GameObject: - component: {fileID: 1098173225717622924} - component: {fileID: 3141292696673982546} - component: {fileID: 5948271423698091598} - m_Layer: 9 + m_Layer: 0 m_Name: Portal m_TagString: Untagged m_Icon: {fileID: 0} @@ -31,6 +31,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: 0} m_LocalScale: {x: 2, y: 2, z: 2} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1355348187805494562} m_Father: {fileID: 0} @@ -55,10 +56,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -83,6 +86,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!136 &1098173225717622924 CapsuleCollider: m_ObjectHideFlags: 0 @@ -110,9 +114,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 1770499856 serverOnly: 0 visible: 0 - m_AssetId: c624c75494b4d7d4086b9212f897e56a hasSpawned: 0 --- !u!114 &5948271423698091598 MonoBehaviour: @@ -126,11 +130,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 0e680878006965146a8f9d85834c4d1c, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 destinationScene: startPosition: {x: 0, y: 0, z: 0} - label: {fileID: 5446595135713311426} + label: {fileID: 8197110483235692531} + labelText: --- !u!1 &5961932215084527574 GameObject: m_ObjectHideFlags: 0 @@ -141,9 +147,8 @@ GameObject: m_Component: - component: {fileID: 1355348187805494562} - component: {fileID: 5428053421152709616} - - component: {fileID: 4525528713057871397} - - component: {fileID: 5446595135713311426} - component: {fileID: 3243959486819493908} + - component: {fileID: 8197110483235692531} m_Layer: 9 m_Name: Label_TMP m_TagString: Untagged @@ -161,6 +166,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1098173225717622921} m_RootOrder: 0 @@ -181,14 +187,16 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 0 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: - - {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + - {fileID: 10100, guid: 0000000000000000e000000000000000, type: 0} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -209,114 +217,38 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 ---- !u!222 &4525528713057871397 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5961932215084527574} - m_CullTransparentMesh: 0 ---- !u!114 &5446595135713311426 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &3243959486819493908 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5961932215084527574} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 9541d86e2fd84c1d9990edf0852d74ab, type: 3} + m_Script: {fileID: 11500000, guid: cc58300ee45438a418d9e32957fdc0c0, type: 3} m_Name: m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_text: Sublevel2 - m_isRightToLeft: 0 - m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} - m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} - m_fontSharedMaterials: [] - m_fontMaterial: {fileID: 0} - m_fontMaterials: [] - m_fontColor32: - serializedVersion: 2 - rgba: 4288619804 - m_fontColor: {r: 0.108668566, g: 0.14359462, b: 0.6226415, a: 1} - m_enableVertexGradient: 0 - m_colorMode: 3 - m_fontColorGradient: - topLeft: {r: 1, g: 1, b: 1, a: 1} - topRight: {r: 1, g: 1, b: 1, a: 1} - bottomLeft: {r: 1, g: 1, b: 1, a: 1} - bottomRight: {r: 1, g: 1, b: 1, a: 1} - m_fontColorGradientPreset: {fileID: 0} - m_spriteAsset: {fileID: 0} - m_tintAllSprites: 0 - m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: -1183493901 - m_overrideHtmlColors: 0 - m_faceColor: - serializedVersion: 2 - rgba: 4294967295 - m_fontSize: 4 - m_fontSizeBase: 4 - m_fontWeight: 400 - m_enableAutoSizing: 0 - m_fontSizeMin: 18 - m_fontSizeMax: 72 - m_fontStyle: 5 - m_HorizontalAlignment: 2 - m_VerticalAlignment: 512 - m_textAlignment: 65535 - m_characterSpacing: 0 - m_wordSpacing: 0 - m_lineSpacing: 0 - m_lineSpacingMax: 0 - m_paragraphSpacing: 0 - m_charWidthMaxAdj: 0 - m_enableWordWrapping: 1 - m_wordWrappingRatios: 0.4 - m_overflowMode: 0 - m_linkedTextComponent: {fileID: 0} - parentLinkedComponent: {fileID: 0} - m_enableKerning: 1 - m_enableExtraPadding: 1 - checkPaddingRequired: 0 - m_isRichText: 1 - m_parseCtrlCharacters: 1 - m_isOrthographic: 0 - m_isCullingEnabled: 0 - m_horizontalMapping: 0 - m_verticalMapping: 0 - m_uvLineOffset: 0 - m_geometrySortingOrder: 0 - m_IsTextObjectScaleStatic: 0 - m_VertexBufferAutoSizeReduction: 0 - m_useMaxVisibleDescender: 1 - m_pageToDisplay: 1 - m_margin: {x: 0, y: 0, z: 0, w: 0} - m_isUsingLegacyAnimationComponent: 0 - m_isVolumetricText: 0 - m_hasFontAssetChanged: 0 - m_renderer: {fileID: 5428053421152709616} - m_maskType: 0 - _SortingLayer: 0 - _SortingLayerID: 0 - _SortingOrder: 0 ---- !u!114 &3243959486819493908 -MonoBehaviour: +--- !u!102 &8197110483235692531 +TextMesh: + serializedVersion: 3 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5961932215084527574} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: cc58300ee45438a418d9e32957fdc0c0, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Text: + m_OffsetZ: 0 + m_CharacterSize: 0.05 + m_LineSpacing: 1 + m_Anchor: 4 + m_Alignment: 1 + m_TabSize: 4 + m_FontSize: 100 + m_FontStyle: 0 + m_RichText: 1 + m_Font: {fileID: 0} + m_Color: + serializedVersion: 2 + rgba: 4294967295 diff --git a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Sphere.prefab b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Sphere.prefab index fdc50b4..ab1abd4 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Sphere.prefab +++ b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/Sphere.prefab @@ -31,6 +31,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -54,10 +55,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -82,6 +85,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!135 &2188792038427236745 SphereCollider: m_ObjectHideFlags: 0 @@ -108,9 +112,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 3918015202 serverOnly: 0 visible: 0 - m_AssetId: e588080aa542be54d9ca9d5c734dc9ee hasSpawned: 0 --- !u!114 &1758063572567377699 MonoBehaviour: @@ -124,6 +128,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a91a718a70d01b347b75cb768a6f1a92, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: diff --git a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/StartPoint.prefab b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/StartPoint.prefab index dc890d6..c70961b 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Prefabs/StartPoint.prefab +++ b/Assets/Mirror/Examples/AdditiveLevels/Prefabs/StartPoint.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 2, y: 0.05, z: 2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -51,10 +52,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -79,3 +82,4 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scenes/Offline.unity b/Assets/Mirror/Examples/AdditiveLevels/Scenes/Offline.unity index cc0e17a..bcaed3e 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scenes/Offline.unity +++ b/Assets/Mirror/Examples/AdditiveLevels/Scenes/Offline.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 1 m_GISettings: serializedVersion: 2 @@ -98,7 +98,7 @@ LightmapSettings: m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_UseShadowmask: 1 + m_LightingSettings: {fileID: 0} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -118,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -173,8 +175,8 @@ Camera: near clip plane: 0.3 far clip plane: 1000 field of view: 60 - orthographic: 0 - orthographic size: 5 + orthographic: 1 + orthographic size: 15 m_Depth: -1 m_CullingMask: serializedVersion: 2 @@ -200,6 +202,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -237,6 +240,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -269,6 +273,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1300359894} m_RootOrder: 0 @@ -330,10 +335,12 @@ MonoBehaviour: Interval: 10 Timeout: 10000 FastResend: 2 - CongestionWindow: 0 SendWindowSize: 4096 ReceiveWindowSize: 4096 - NonAlloc: 1 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 298449 + UnreliableMaxMessageSize: 1199 debugLog: 0 statisticsGUI: 0 statisticsLog: 0 @@ -350,21 +357,21 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: dontDestroyOnLoad: 1 - PersistNetworkManagerToOfflineScene: 0 runInBackground: 1 autoStartServerBuild: 1 - serverTickRate: 30 + autoConnectClientBuild: 0 + sendRate: 30 offlineScene: Assets/Mirror/Examples/AdditiveLevels/Scenes/Offline.unity onlineScene: Assets/Mirror/Examples/AdditiveLevels/Scenes/Online.unity transport: {fileID: 1074858615} networkAddress: localhost maxConnections: 100 authenticator: {fileID: 0} - playerPrefab: {fileID: 7619140271685878370, guid: 9f0094c1325091d42a558274b947221f, - type: 3} + playerPrefab: {fileID: 7619140271685878370, guid: 9f0094c1325091d42a558274b947221f, type: 3} autoCreatePlayer: 0 playerSpawnMethod: 1 spawnPrefabs: [] + timeInterpolationGui: 0 additiveScenes: - Assets/Mirror/Examples/AdditiveLevels/Scenes/SubLevel1.unity - Assets/Mirror/Examples/AdditiveLevels/Scenes/SubLevel2.unity @@ -379,6 +386,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1300359894} m_Father: {fileID: 0} @@ -452,6 +460,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!223 &1300359893 Canvas: m_ObjectHideFlags: 0 @@ -483,6 +492,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1040404847} m_Father: {fileID: 1074858617} diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scenes/Online.unity b/Assets/Mirror/Examples/AdditiveLevels/Scenes/Online.unity index 4b02b95..4ec4023 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scenes/Online.unity +++ b/Assets/Mirror/Examples/AdditiveLevels/Scenes/Online.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 1 m_GISettings: serializedVersion: 2 @@ -97,9 +97,8 @@ LightmapSettings: m_ExportTrainingData: 0 m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 - m_LightingDataAsset: {fileID: 112000002, guid: 7868b86bff1943140826320e66c66468, - type: 2} - m_UseShadowmask: 1 + m_LightingDataAsset: {fileID: 112000002, guid: 7868b86bff1943140826320e66c66468, type: 2} + m_LightingSettings: {fileID: 4890085278179872738, guid: b14d22a581510774fa4c3d6017f61944, type: 2} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -119,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -198,6 +199,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &203151411 @@ -210,6 +212,7 @@ Transform: m_LocalRotation: {x: 0.55403227, y: -0.21201213, z: 0.14845248, w: 0.79124016} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 1 @@ -266,8 +269,8 @@ Camera: near clip plane: 0.3 far clip plane: 1000 field of view: 60 - orthographic: 0 - orthographic size: 5 + orthographic: 1 + orthographic size: 15 m_Depth: -1 m_CullingMask: serializedVersion: 2 @@ -293,120 +296,11 @@ Transform: m_LocalRotation: {x: 0, y: -0.2658926, z: 0, w: 0.9640027} m_LocalPosition: {x: 3.84, y: 1.91, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: -30.84, z: 0} ---- !u!1 &499382192 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 499382200} - - component: {fileID: 499382199} - - component: {fileID: 499382197} - - component: {fileID: 499382196} - - component: {fileID: 499382195} - - component: {fileID: 499382194} - - component: {fileID: 499382193} - m_Layer: 0 - m_Name: SafetyPlane - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!65 &499382193 -BoxCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_Material: {fileID: 0} - m_IsTrigger: 0 - m_Enabled: 1 - serializedVersion: 2 - m_Size: {x: 10, y: 10, z: 0.1} - m_Center: {x: 0, y: 5, z: 5} ---- !u!65 &499382194 -BoxCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_Material: {fileID: 0} - m_IsTrigger: 0 - m_Enabled: 1 - serializedVersion: 2 - m_Size: {x: 0.1, y: 10, z: 10} - m_Center: {x: 5, y: 5, z: 0} ---- !u!65 &499382195 -BoxCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_Material: {fileID: 0} - m_IsTrigger: 0 - m_Enabled: 1 - serializedVersion: 2 - m_Size: {x: 0.1, y: 10, z: 10} - m_Center: {x: -5, y: 5, z: 0} ---- !u!65 &499382196 -BoxCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_Material: {fileID: 0} - m_IsTrigger: 0 - m_Enabled: 1 - serializedVersion: 2 - m_Size: {x: 10, y: 10, z: 0.1} - m_Center: {x: 0, y: 5, z: -5} ---- !u!64 &499382197 -MeshCollider: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_Material: {fileID: 0} - m_IsTrigger: 0 - m_Enabled: 1 - serializedVersion: 4 - m_Convex: 0 - m_CookingOptions: 30 - m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} ---- !u!33 &499382199 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} ---- !u!4 &499382200 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 499382192} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 3, y: 1, z: 3} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 6 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1110529627 GameObject: m_ObjectHideFlags: 0 @@ -483,6 +377,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &1110529629 @@ -495,6 +390,7 @@ Transform: m_LocalRotation: {x: 0.9848078, y: -0, z: -0, w: -0.17364809} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 3 @@ -575,6 +471,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &1309024826 @@ -587,6 +484,7 @@ Transform: m_LocalRotation: {x: 0.28678825, y: -0.70940644, z: -0.4967319, w: -0.4095759} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 4 @@ -667,6 +565,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &1606864535 @@ -679,6 +578,7 @@ Transform: m_LocalRotation: {x: 0.9063079, y: 0, z: 0, w: 0.42261827} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 2 @@ -713,6 +613,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} m_Name: m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 m_HorizontalAxis: Horizontal m_VerticalAxis: Vertical m_SubmitButton: Submit @@ -745,6 +646,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 5 diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scenes/OnlineSettings.lighting b/Assets/Mirror/Examples/AdditiveLevels/Scenes/OnlineSettings.lighting new file mode 100644 index 0000000..fd67916 --- /dev/null +++ b/Assets/Mirror/Examples/AdditiveLevels/Scenes/OnlineSettings.lighting @@ -0,0 +1,63 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!850595691 &4890085278179872738 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: OnlineSettings + serializedVersion: 3 + m_GIWorkflowMode: 1 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_TextureCompression: 1 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 1 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scenes/OnlineSettings.lighting.meta b/Assets/Mirror/Examples/AdditiveLevels/Scenes/OnlineSettings.lighting.meta new file mode 100644 index 0000000..96b73eb --- /dev/null +++ b/Assets/Mirror/Examples/AdditiveLevels/Scenes/OnlineSettings.lighting.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b14d22a581510774fa4c3d6017f61944 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 4890085278179872738 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scripts/AdditiveLevelsNetworkManager.cs b/Assets/Mirror/Examples/AdditiveLevels/Scripts/AdditiveLevelsNetworkManager.cs index 7dffafe..00d459f 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scripts/AdditiveLevelsNetworkManager.cs +++ b/Assets/Mirror/Examples/AdditiveLevels/Scripts/AdditiveLevelsNetworkManager.cs @@ -7,6 +7,18 @@ namespace Mirror.Examples.AdditiveLevels [AddComponentMenu("")] public class AdditiveLevelsNetworkManager : NetworkManager { + public static new AdditiveLevelsNetworkManager singleton { get; private set; } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + singleton = this; + } + [Header("Additive Scenes - First is start scene")] [Scene, Tooltip("Add additive scenes here.\nFirst entry will be players' start scene")] @@ -72,7 +84,7 @@ IEnumerator LoadAdditive(string sceneName) isInTransition = true; // This will return immediately if already faded in - // e.g. by UnloadAdditive above or by default startup state + // e.g. by UnloadAdditive or by default startup state yield return fadeInOut.FadeIn(); // host client is on server...don't load the additive scene again @@ -91,6 +103,7 @@ IEnumerator LoadAdditive(string sceneName) OnClientSceneChanged(); + // Reveal the new scene content. yield return fadeInOut.FadeOut(); } @@ -99,9 +112,10 @@ IEnumerator UnloadAdditive(string sceneName) isInTransition = true; // This will return immediately if already faded in - // e.g. by LoadAdditive above or by default startup state + // e.g. by LoadAdditive above or by default startup state. yield return fadeInOut.FadeIn(); + // host client is on server...don't unload the additive scene here. if (mode == NetworkManagerMode.ClientOnly) { yield return SceneManager.UnloadSceneAsync(sceneName); @@ -115,8 +129,8 @@ IEnumerator UnloadAdditive(string sceneName) OnClientSceneChanged(); // There is no call to FadeOut here on purpose. - // Expectation is that a LoadAdditive will follow - // that will call FadeOut after that scene loads. + // Expectation is that a LoadAdditive or full scene change + // will follow that will call FadeOut after that scene loads. } /// @@ -126,8 +140,6 @@ IEnumerator UnloadAdditive(string sceneName) /// The network connection that the scene change message arrived on. public override void OnClientSceneChanged() { - //Debug.Log($"{System.DateTime.Now:HH:mm:ss:fff} OnClientSceneChanged {isInTransition}"); - // Only call the base method if not in a transition. // This will be called from DoTransition after setting doingTransition to false // but will also be called first by Mirror when the scene loading finishes. @@ -146,8 +158,6 @@ public override void OnClientSceneChanged() /// Connection from client. public override void OnServerReady(NetworkConnectionToClient conn) { - //Debug.Log($"OnServerReady {conn} {conn.identity}"); - // This fires from a Ready message client sends to server after loading the online scene base.OnServerReady(conn); diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scripts/FadeInOut.cs b/Assets/Mirror/Examples/AdditiveLevels/Scripts/FadeInOut.cs index c378980..e9f781a 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scripts/FadeInOut.cs +++ b/Assets/Mirror/Examples/AdditiveLevels/Scripts/FadeInOut.cs @@ -25,8 +25,6 @@ void Awake() public IEnumerator FadeIn() { - //Debug.Log($"{System.DateTime.Now:HH:mm:ss:fff} FadeIn - fading image in {fadeImage.color.a}"); - float alpha = fadeImage.color.a; while (alpha < 1) @@ -36,14 +34,10 @@ public IEnumerator FadeIn() fadeColor.a = alpha; fadeImage.color = fadeColor; } - - //Debug.Log($"{System.DateTime.Now:HH:mm:ss:fff} FadeIn - done fading"); } public IEnumerator FadeOut() { - //Debug.Log($"{System.DateTime.Now:HH:mm:ss:fff} FadeOut - fading image out {fadeImage.color.a}"); - float alpha = fadeImage.color.a; while (alpha > 0) @@ -53,8 +47,6 @@ public IEnumerator FadeOut() fadeColor.a = alpha; fadeImage.color = fadeColor; } - - //Debug.Log($"{System.DateTime.Now:HH:mm:ss:fff} FadeOut - done fading"); } } } diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scripts/LookAtMainCamera.cs b/Assets/Mirror/Examples/AdditiveLevels/Scripts/LookAtMainCamera.cs index 2295416..ccb88fb 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scripts/LookAtMainCamera.cs +++ b/Assets/Mirror/Examples/AdditiveLevels/Scripts/LookAtMainCamera.cs @@ -5,7 +5,14 @@ namespace Mirror.Examples.AdditiveLevels // This script is attached to portal labels to keep them facing the camera public class LookAtMainCamera : MonoBehaviour { + // This will be enabled by Portal script in OnStartClient + void OnValidate() + { + this.enabled = false; + } + // LateUpdate so that all camera updates are finished. + [ClientCallback] void LateUpdate() { transform.forward = Camera.main.transform.forward; diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerCamera.cs b/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerCamera.cs index ed9ad58..f0e3968 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerCamera.cs +++ b/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerCamera.cs @@ -24,6 +24,8 @@ public override void OnStartLocalPlayer() mainCam.transform.localPosition = new Vector3(0f, 3f, -8f); mainCam.transform.localEulerAngles = new Vector3(10f, 0f, 0f); } + else + Debug.LogWarning("PlayerCamera: Could not find a camera in scene with 'MainCamera' tag."); } public override void OnStopLocalPlayer() @@ -33,6 +35,7 @@ public override void OnStopLocalPlayer() mainCam.transform.SetParent(null); SceneManager.MoveGameObjectToScene(mainCam.gameObject, SceneManager.GetActiveScene()); mainCam.orthographic = true; + mainCam.orthographicSize = 15f; mainCam.transform.localPosition = new Vector3(0f, 70f, 0f); mainCam.transform.localEulerAngles = new Vector3(90f, 0f, 0f); } diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerController.cs b/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerController.cs index 8cabd66..01bd142 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerController.cs +++ b/Assets/Mirror/Examples/AdditiveLevels/Scripts/PlayerController.cs @@ -8,88 +8,177 @@ namespace Mirror.Examples.AdditiveLevels [RequireComponent(typeof(Rigidbody))] public class PlayerController : NetworkBehaviour { + public enum GroundState : byte { Jumping, Falling, Grounded } + + [Header("Avatar Components")] public CharacterController characterController; - [Header("Movement Settings")] - public float moveSpeed = 8f; - public float turnSensitivity = 5f; + [Header("Movement")] + [Range(1, 20)] + public float moveSpeedMultiplier = 8f; + + [Header("Turning")] + [Range(1f, 200f)] public float maxTurnSpeed = 100f; + [Range(.5f, 5f)] + public float turnDelta = 3f; + + [Header("Jumping")] + [Range(0.1f, 1f)] + public float initialJumpSpeed = 0.2f; + [Range(1f, 10f)] + public float maxJumpSpeed = 5f; + [Range(0.1f, 1f)] + public float jumpDelta = 0.2f; + + [Header("Diagnostics - Do Not Modify")] + public GroundState groundState = GroundState.Grounded; - [Header("Diagnostics")] + [Range(-1f, 1f)] public float horizontal; + [Range(-1f, 1f)] public float vertical; - public float turn; + + [Range(-200f, 200f)] + public float turnSpeed; + + [Range(-10f, 10f)] public float jumpSpeed; - public bool isGrounded = true; - public bool isFalling; - public Vector3 velocity; + + [Range(-1.5f, 1.5f)] + public float animVelocity; + + [Range(-1.5f, 1.5f)] + public float animRotation; + + public Vector3Int velocity; + public Vector3 direction; void OnValidate() { if (characterController == null) characterController = GetComponent(); + // Override CharacterController default values characterController.enabled = false; + characterController.skinWidth = 0.02f; + characterController.minMoveDistance = 0f; + GetComponent().isKinematic = true; - GetComponent().clientAuthority = true; + + this.enabled = false; } - public override void OnStartLocalPlayer() + public override void OnStartAuthority() { characterController.enabled = true; + this.enabled = true; + } + + public override void OnStopAuthority() + { + this.enabled = false; + characterController.enabled = false; } void Update() { - if (!isLocalPlayer || characterController == null || !characterController.enabled) + if (!characterController.enabled) return; - horizontal = Input.GetAxis("Horizontal"); - vertical = Input.GetAxis("Vertical"); + HandleTurning(); + HandleJumping(); + HandleMove(); + + // Reset ground state + if (characterController.isGrounded) + groundState = GroundState.Grounded; + else if (groundState != GroundState.Jumping) + groundState = GroundState.Falling; + + // Diagnostic velocity...FloorToInt for display purposes + velocity = Vector3Int.FloorToInt(characterController.velocity); + } - // Q and E cancel each other out, reducing the turn to zero + // TODO: Turning works while airborne...feature? + void HandleTurning() + { + // Q and E cancel each other out, reducing the turn to zero. if (Input.GetKey(KeyCode.Q)) - turn = Mathf.MoveTowards(turn, -maxTurnSpeed, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, -maxTurnSpeed, turnDelta); if (Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, maxTurnSpeed, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, maxTurnSpeed, turnDelta); + + // If both pressed, reduce turning speed toward zero. if (Input.GetKey(KeyCode.Q) && Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, 0, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, 0, turnDelta); + + // If neither pressed, reduce turning speed toward zero. if (!Input.GetKey(KeyCode.Q) && !Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, 0, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, 0, turnDelta); - if (isGrounded) - isFalling = false; + transform.Rotate(0f, turnSpeed * Time.deltaTime, 0f); + } - if ((isGrounded || !isFalling) && jumpSpeed < 1f && Input.GetKey(KeyCode.Space)) + void HandleJumping() + { + // Handle variable force jumping. + // Jump starts with initial power on takeoff, and jumps higher / longer + // as player holds spacebar. Jump power is increased by a diminishing amout + // every frame until it reaches maxJumpSpeed, or player releases the spacebar, + // and then changes to the falling state until it gets grounded. + if (groundState != GroundState.Falling && Input.GetKey(KeyCode.Space)) { - jumpSpeed = Mathf.Lerp(jumpSpeed, 1f, 0.5f); + if (groundState != GroundState.Jumping) + { + // Start jump at initial power. + groundState = GroundState.Jumping; + jumpSpeed = initialJumpSpeed; + } + else + // Jumping has already started...increase power toward maxJumpSpeed over time. + jumpSpeed = Mathf.MoveTowards(jumpSpeed, maxJumpSpeed, jumpDelta); + + // If power has reached maxJumpSpeed, change to falling until grounded. + // This prevents over-applying jump power while already in the air. + if (jumpSpeed == maxJumpSpeed) + groundState = GroundState.Falling; } - else if (!isGrounded) + else if (groundState != GroundState.Grounded) { - isFalling = true; - jumpSpeed = 0; + // handles running off a cliff and/or player released Spacebar. + groundState = GroundState.Falling; + jumpSpeed = Mathf.Min(jumpSpeed, maxJumpSpeed); + jumpSpeed += Physics.gravity.y * Time.deltaTime; } + else + jumpSpeed = Physics.gravity.y * Time.deltaTime; } - void FixedUpdate() + // TODO: Directional input works while airborne...feature? + void HandleMove() { - if (!isLocalPlayer || characterController == null || !characterController.enabled) - return; + // Capture inputs + horizontal = Input.GetAxis("Horizontal"); + vertical = Input.GetAxis("Vertical"); - transform.Rotate(0f, turn * Time.fixedDeltaTime, 0f); + // Create initial direction vector without jumpSpeed (y-axis). + direction = new Vector3(horizontal, 0f, vertical); - Vector3 direction = new Vector3(horizontal, jumpSpeed, vertical); + // Clamp so diagonal strafing isn't a speed advantage. direction = Vector3.ClampMagnitude(direction, 1f); + + // Transforms direction from local space to world space. direction = transform.TransformDirection(direction); - direction *= moveSpeed; - if (jumpSpeed > 0) - characterController.Move(direction * Time.fixedDeltaTime); - else - characterController.SimpleMove(direction); + // Multiply for desired ground speed. + direction *= moveSpeedMultiplier; + + // Add jumpSpeed to direction as last step. + direction.y = jumpSpeed; - isGrounded = characterController.isGrounded; - velocity = characterController.velocity; + // Finally move the character. + characterController.Move(direction * Time.deltaTime); } } } diff --git a/Assets/Mirror/Examples/AdditiveLevels/Scripts/Portal.cs b/Assets/Mirror/Examples/AdditiveLevels/Scripts/Portal.cs index e0df00c..9625270 100644 --- a/Assets/Mirror/Examples/AdditiveLevels/Scripts/Portal.cs +++ b/Assets/Mirror/Examples/AdditiveLevels/Scripts/Portal.cs @@ -15,7 +15,7 @@ public class Portal : NetworkBehaviour public Vector3 startPosition; [Tooltip("Reference to child TMP label")] - public TMPro.TextMeshPro label; + public TextMesh label; // don't depend on TMPro. 2019 errors. [SyncVar(hook = nameof(OnLabelTextChanged))] public string labelText; @@ -36,17 +36,21 @@ public override void OnStartServer() labelText = Regex.Replace(labelText, @"\B[A-Z0-9]+", " $0"); } - // Note that I have created layers called Player(8) and Portal(9) and set them + public override void OnStartClient() + { + if (label.TryGetComponent(out LookAtMainCamera lookAtMainCamera)) + lookAtMainCamera.enabled = true; + } + + // Note that I have created layers called Player(6) and Portal(7) and set them // up in the Physics collision matrix so only Player collides with Portal. void OnTriggerEnter(Collider other) { // tag check in case you didn't set up the layers and matrix as noted above if (!other.CompareTag("Player")) return; - //Debug.Log($"{System.DateTime.Now:HH:mm:ss:fff} Portal::OnTriggerEnter {gameObject.name} in {gameObject.scene.name}"); - // applies to host client on server and remote clients - if (other.TryGetComponent(out PlayerController playerController)) + if (other.TryGetComponent(out PlayerController playerController)) playerController.enabled = false; if (isServer) @@ -56,7 +60,7 @@ void OnTriggerEnter(Collider other) [ServerCallback] IEnumerator SendPlayerToNewScene(GameObject player) { - if (player.TryGetComponent(out NetworkIdentity identity)) + if (player.TryGetComponent(out NetworkIdentity identity)) { NetworkConnectionToClient conn = identity.connectionToClient; if (conn == null) yield break; @@ -66,7 +70,6 @@ IEnumerator SendPlayerToNewScene(GameObject player) yield return waitForFade; - //Debug.Log($"SendPlayerToNewScene RemovePlayerForConnection {conn} netId:{conn.identity.netId}"); NetworkServer.RemovePlayerForConnection(conn, false); // reposition player on server and client @@ -79,11 +82,10 @@ IEnumerator SendPlayerToNewScene(GameObject player) // Tell client to load the new subscene with custom handling (see NetworkManager::OnClientChangeScene). conn.Send(new SceneMessage { sceneName = destinationScene, sceneOperation = SceneOperation.LoadAdditive, customHandling = true }); - //Debug.Log($"SendPlayerToNewScene AddPlayerForConnection {conn} netId:{conn.identity.netId}"); NetworkServer.AddPlayerForConnection(conn, player); // host client would have been disabled by OnTriggerEnter above - if (NetworkClient.localPlayer != null && NetworkClient.localPlayer.TryGetComponent(out PlayerController playerController)) + if (NetworkClient.localPlayer != null && NetworkClient.localPlayer.TryGetComponent(out PlayerController playerController)) playerController.enabled = true; } } diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Capsule.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Capsule.prefab index 383cb51..d26a5ae 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Capsule.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Capsule.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1076878374699499735} m_RootOrder: 0 @@ -51,10 +52,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -79,6 +82,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &1076878374699499732 GameObject: m_ObjectHideFlags: 0 @@ -107,6 +111,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 6907979021268419569} m_Father: {fileID: 0} @@ -125,9 +130,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 3033632540 serverOnly: 0 visible: 0 - m_AssetId: e1971f4a8c7661546bc509b44bd91b80 hasSpawned: 0 --- !u!136 &1076878374699499734 CapsuleCollider: diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cube.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cube.prefab index f7b66a8..d01a18a 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cube.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cube.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 2, y: 2, z: 2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5623359707189648426} m_RootOrder: 0 @@ -51,10 +52,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -79,6 +82,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &5623359707189648430 GameObject: m_ObjectHideFlags: 0 @@ -107,6 +111,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: 2} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 375242947413242802} m_Father: {fileID: 0} @@ -125,9 +130,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 2233301415 serverOnly: 0 visible: 0 - m_AssetId: 4ff300cf6bb3c6342a9552c4f18788c8 hasSpawned: 0 --- !u!65 &963943828455949898 BoxCollider: diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cylinder.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cylinder.prefab index d0eb27d..95b894f 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cylinder.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Cylinder.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5318406868242510088} m_Father: {fileID: 0} @@ -46,9 +47,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 3042034178 serverOnly: 0 visible: 0 - m_AssetId: 12a4c14e672c00b4b840f937d824b890 hasSpawned: 0 --- !u!136 &6852530814182375313 CapsuleCollider: @@ -92,6 +93,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6852530814182375316} m_RootOrder: 0 @@ -115,10 +117,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -143,3 +147,4 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Player.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Player.prefab index 176b7bd..b1c3b95 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Player.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3254954141432383832} m_Father: {fileID: 5328458565928408179} @@ -52,10 +53,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -80,6 +83,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &5815001218983416211 GameObject: m_ObjectHideFlags: 0 @@ -108,6 +112,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5} m_LocalScale: {x: 0.5, y: 0.1, z: 0.2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 9057824595171805708} m_RootOrder: 0 @@ -131,10 +136,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -159,6 +166,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &8872462076811691049 GameObject: m_ObjectHideFlags: 0 @@ -177,9 +185,9 @@ GameObject: - component: {fileID: 3175779197224890082} - component: {fileID: 3086414693581178039} - component: {fileID: -4778368485878020104} - m_Layer: 8 + m_Layer: 0 m_Name: Player - m_TagString: Untagged + m_TagString: Player m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 @@ -194,6 +202,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1.08, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 9057824595171805708} m_Father: {fileID: 0} @@ -212,9 +221,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 2014258290 serverOnly: 0 visible: 0 - m_AssetId: a5bdca0a2315d43499be7dcef473fbc7 hasSpawned: 0 --- !u!114 &887491563423388292 MonoBehaviour: @@ -228,28 +237,22 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 1 syncMode: 0 - syncInterval: 0.1 - clientAuthority: 1 - sendInterval: 0.05 + syncInterval: 0 + target: {fileID: 5328458565928408179} + clientAuthority: 0 syncPosition: 1 syncRotation: 1 syncScale: 0 - interpolatePosition: 1 - interpolateRotation: 1 - interpolateScale: 0 - bufferTimeMultiplier: 1 - bufferSizeLimit: 64 - catchupThreshold: 4 - catchupMultiplier: 0.1 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChange: 1 bufferResetMultiplier: 5 positionSensitivity: 0.01 rotationSensitivity: 0.01 scaleSensitivity: 0.01 - showGizmos: 0 - showOverlay: 0 - overlayColor: {r: 0, g: 0, b: 0, a: 0.5} --- !u!114 &-2082299755652640335 MonoBehaviour: m_ObjectHideFlags: 0 @@ -262,6 +265,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 437e9ed1a55931845bef07e2f5ef57e0, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 --- !u!114 &8704659178864205755 @@ -271,24 +275,31 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8872462076811691049} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: e8f68561248aaca4fb96847ce24742ee, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 characterController: {fileID: 8993127209816276930} - moveSpeed: 8 - turnSensitivity: 5 + controlsCanvas: {fileID: 0} + moveSpeedMultiplier: 8 maxTurnSpeed: 100 + turnDelta: 1 + initialJumpSpeed: 0.2 + maxJumpSpeed: 5 + jumpDelta: 0.2 + groundState: 2 horizontal: 0 vertical: 0 - turn: 0 + turnSpeed: 0 jumpSpeed: 0 - isGrounded: 1 - isFalling: 0 + animVelocity: 0 + animRotation: 0 velocity: {x: 0, y: 0, z: 0} + direction: {x: 0, y: 0, z: 0} --- !u!143 &8993127209816276930 CharacterController: m_ObjectHideFlags: 0 @@ -304,8 +315,8 @@ CharacterController: m_Radius: 0.5 m_SlopeLimit: 45 m_StepOffset: 0.3 - m_SkinWidth: 0.08 - m_MinMoveDistance: 0.001 + m_SkinWidth: 0.02 + m_MinMoveDistance: 0 m_Center: {x: 0, y: 0, z: 0} --- !u!136 &1143206540915927667 CapsuleCollider: @@ -318,7 +329,7 @@ CapsuleCollider: m_IsTrigger: 0 m_Enabled: 1 m_Radius: 0.5 - m_Height: 1 + m_Height: 2 m_Direction: 1 m_Center: {x: 0, y: 0, z: 0} --- !u!54 &3175779197224890082 @@ -349,6 +360,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 42d1f80407105ee4f960f0b51e89452d, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: @@ -366,6 +378,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: b2e242ee38a14076a39934172a19079b, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 visRange: 40 diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Sphere.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Sphere.prefab index 434ced3..da84aeb 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Sphere.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Sphere.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1367021456138387611} m_Father: {fileID: 0} @@ -46,9 +47,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 1391161870 serverOnly: 0 visible: 0 - m_AssetId: f6d08eb9a8e35d84fa30a7e3ae64181a hasSpawned: 0 --- !u!135 &855244094988030904 SphereCollider: @@ -91,6 +92,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 2, y: 2, z: 2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 855244094988030909} m_RootOrder: 0 @@ -114,10 +116,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -142,3 +146,4 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Tank.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Tank.prefab index 47f5856..c01c2bd 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Tank.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Tank.prefab @@ -30,6 +30,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 5, y: 5, z: 5} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3234001708628876000} - {fileID: 1042389410631263445} @@ -49,9 +50,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 635825396 serverOnly: 0 visible: 0 - m_AssetId: ab222ed73ada1ac4ba2f61e843d7627c hasSpawned: 0 --- !u!114 &160176461 MonoBehaviour: @@ -65,13 +66,14 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 7f6f3bf89aa97405989c802ba270f815, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 clientAuthority: 0 animator: {fileID: 160176460} --- !u!95 &160176460 Animator: - serializedVersion: 3 + serializedVersion: 5 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -84,10 +86,11 @@ Animator: m_UpdateMode: 0 m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 + m_StabilizeFeet: 0 m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 - m_KeepAnimatorControllerStateOnDisable: 0 + m_KeepAnimatorStateOnDisable: 0 --- !u!114 &160176462 MonoBehaviour: m_ObjectHideFlags: 0 @@ -100,6 +103,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 7a25c54cd35eb284eb6b8ed19cf60443, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 rotation: {x: 0, y: 0, z: 0, w: 0} @@ -130,6 +134,7 @@ Transform: m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0.0021921142, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5371032128924763904} m_RootOrder: 0 @@ -160,6 +165,7 @@ Transform: m_LocalRotation: {x: -0.5, y: 0.5, z: 0.49999994, w: 0.50000006} m_LocalPosition: {x: -0, y: 0.0011627917, z: 0.0000000010728836} m_LocalScale: {x: 1, y: 0.99999994, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 9163197381092130014} m_Father: {fileID: 847897825935598517} @@ -191,6 +197,7 @@ Transform: m_LocalRotation: {x: 0, y: -0.000000119209275, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0.0010293524, z: 0} m_LocalScale: {x: 1, y: 0.99999994, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1517159280684637724} m_Father: {fileID: 1703734463393124925} @@ -222,6 +229,7 @@ Transform: m_LocalRotation: {x: -0.5, y: 0.5, z: 0.49999994, w: 0.50000006} m_LocalPosition: {x: -0, y: 0.0011627917, z: -0.0026999994} m_LocalScale: {x: 1, y: 0.99999994, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 6048638457609172120} m_Father: {fileID: 847897825935598517} @@ -253,6 +261,7 @@ Transform: m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0.0063666296, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1517159280684637724} m_RootOrder: 0 @@ -285,6 +294,7 @@ Transform: m_LocalRotation: {x: -0.7071068, y: 0, z: -0, w: 0.7071067} m_LocalPosition: {x: -0, y: 0, z: 0} m_LocalScale: {x: 100, y: 100, z: 100} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 160176456} m_RootOrder: 0 @@ -300,10 +310,12 @@ SkinnedMeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 0 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -387,6 +399,7 @@ Transform: m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071067} m_LocalPosition: {x: -0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1703734463393124925} - {fileID: 7124543900430328667} @@ -421,6 +434,7 @@ Transform: m_LocalRotation: {x: -0.5, y: 0.5, z: 0.49999994, w: 0.50000006} m_LocalPosition: {x: -0, y: 0.0011627917, z: 0.0027000008} m_LocalScale: {x: 1, y: 0.99999994, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5752532462053122769} m_Father: {fileID: 847897825935598517} @@ -452,6 +466,7 @@ Transform: m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0.0015, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7755980514232685276} m_Father: {fileID: 847897825935598517} @@ -483,6 +498,7 @@ Transform: m_LocalRotation: {x: 0.00000017845065, y: 0.7071068, z: 0.7071067, w: 0.000000009863265} m_LocalPosition: {x: 5.6542865e-10, y: 0.0015793034, z: 0.00237158} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7509984371715941402} m_Father: {fileID: 7755980514232685276} @@ -514,6 +530,7 @@ Transform: m_LocalRotation: {x: -0.7071068, y: 0, z: -0, w: 0.7071067} m_LocalPosition: {x: -0, y: 0, z: 0} m_LocalScale: {x: 100, y: 100, z: 100} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 847897825935598517} m_Father: {fileID: 160176456} @@ -545,6 +562,7 @@ Transform: m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0.0021921142, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 7124543900430328667} m_RootOrder: 0 @@ -575,6 +593,7 @@ Transform: m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0.0021921142, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1766344861363284577} m_RootOrder: 0 diff --git a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Zone.prefab b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Zone.prefab index 84c5aae..a19605b 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Zone.prefab +++ b/Assets/Mirror/Examples/AdditiveScenes/Prefabs/Zone.prefab @@ -11,7 +11,7 @@ GameObject: - component: {fileID: 3460729395543957453} - component: {fileID: 3460729395543957454} - component: {fileID: 3460729395543957443} - m_Layer: 8 + m_Layer: 0 m_Name: Zone m_TagString: Untagged m_Icon: {fileID: 0} @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scenes/MainScene.unity b/Assets/Mirror/Examples/AdditiveScenes/Scenes/MainScene.unity index c230c5b..563d02e 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scenes/MainScene.unity +++ b/Assets/Mirror/Examples/AdditiveScenes/Scenes/MainScene.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 0 m_GISettings: serializedVersion: 2 @@ -97,9 +97,8 @@ LightmapSettings: m_ExportTrainingData: 0 m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 - m_LightingDataAsset: {fileID: 112000004, guid: b287b2046ddc6af4b9ddc48ab35ca3cb, - type: 2} - m_UseShadowmask: 0 + m_LightingDataAsset: {fileID: 112000004, guid: b287b2046ddc6af4b9ddc48ab35ca3cb, type: 2} + m_LightingSettings: {fileID: 489442491} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -119,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -150,6 +151,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.3826836, z: -0, w: -0.92387944} m_LocalPosition: {x: 20, y: 1, z: -20} m_LocalScale: {x: 10, y: 10, z: 10} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 47225731} m_RootOrder: 3 @@ -165,10 +167,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 0 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -193,6 +197,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &34755348 MeshFilter: m_ObjectHideFlags: 0 @@ -227,6 +232,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1727677799} - {fileID: 62078680} @@ -263,6 +269,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.9238796, z: -0, w: -0.38268325} m_LocalPosition: {x: 20, y: 1, z: 20} m_LocalScale: {x: 10, y: 10, z: 10} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 47225731} m_RootOrder: 1 @@ -278,10 +285,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 0 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -306,6 +315,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &62078682 MeshFilter: m_ObjectHideFlags: 0 @@ -389,8 +399,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: ab222ed73ada1ac4ba2f61e843d7627c, type: 3} --- !u!4 &160176456 stripped Transform: - m_CorrespondingSourceObject: {fileID: 160176456, guid: ab222ed73ada1ac4ba2f61e843d7627c, - type: 3} + m_CorrespondingSourceObject: {fileID: 160176456, guid: ab222ed73ada1ac4ba2f61e843d7627c, type: 3} m_PrefabInstance: {fileID: 160176455} m_PrefabAsset: {fileID: 0} --- !u!1 &178547537 @@ -420,6 +429,7 @@ Transform: m_LocalRotation: {x: 0, y: 1, z: 0, w: 0} m_LocalPosition: {x: 0, y: 1.08, z: 20} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1172568542} m_RootOrder: 0 @@ -436,6 +446,68 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!850595691 &489442491 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 4 + m_GIWorkflowMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 0 + m_BakeBackend: 0 + m_LightmapMaxSize: 512 + m_BakeResolution: 10 + m_Padding: 2 + m_LightmapCompression: 3 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 0 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 256 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_PVRTiledBaking: 0 --- !u!1 &534669902 GameObject: m_ObjectHideFlags: 0 @@ -506,6 +578,7 @@ Transform: m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} m_LocalPosition: {x: 0, y: 70, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -538,6 +611,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} m_LocalPosition: {x: -20, y: 1, z: -20} m_LocalScale: {x: 20, y: 20, z: 20} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 47225731} m_RootOrder: 2 @@ -553,10 +627,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 0 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -581,6 +657,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &589935543 MeshFilter: m_ObjectHideFlags: 0 @@ -618,6 +695,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 8, y: 3, z: -8} m_LocalScale: {x: 1, y: 3, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1608696205} m_RootOrder: 1 @@ -647,10 +725,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -675,6 +755,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &612284971 MeshFilter: m_ObjectHideFlags: 0 @@ -710,6 +791,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.7071068, z: -0, w: -0.7071068} m_LocalPosition: {x: 20, y: 1.08, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1172568542} m_RootOrder: 2 @@ -753,6 +835,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1.08, z: -20} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1172568542} m_RootOrder: 3 @@ -776,78 +859,63 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 909502395} m_Modifications: - - target: {fileID: 855244094988030905, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030905, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_Name value: Sphere objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_RootOrder value: 0 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalPosition.x value: -20 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalPosition.y value: 1 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalPosition.z value: 20 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalRotation.w value: 0.38268343 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalRotation.y value: 0.92387956 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 135 objectReference: {fileID: 0} - - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 855244094988030911, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030911, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: sceneId value: 744240842 objectReference: {fileID: 0} - - target: {fileID: 855244094988030911, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030911, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_AssetId value: f6d08eb9a8e35d84fa30a7e3ae64181a objectReference: {fileID: 0} - - target: {fileID: 855244094988030911, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + - target: {fileID: 855244094988030911, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} propertyPath: m_SceneId value: 529586728 objectReference: {fileID: 0} @@ -855,8 +923,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} --- !u!4 &748207075 stripped Transform: - m_CorrespondingSourceObject: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, - type: 3} + m_CorrespondingSourceObject: {fileID: 855244094988030909, guid: f6d08eb9a8e35d84fa30a7e3ae64181a, type: 3} m_PrefabInstance: {fileID: 748207074} m_PrefabAsset: {fileID: 0} --- !u!1 &794922164 @@ -888,6 +955,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 6, z: 0} m_LocalScale: {x: 20, y: 0.2, z: 20} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1608696205} m_RootOrder: 4 @@ -916,10 +984,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -944,6 +1014,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &794922168 MeshFilter: m_ObjectHideFlags: 0 @@ -981,6 +1052,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -8, y: 3, z: 8} m_LocalScale: {x: 1, y: 3, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1608696205} m_RootOrder: 3 @@ -1010,10 +1082,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1038,6 +1112,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &856402107 MeshFilter: m_ObjectHideFlags: 0 @@ -1053,73 +1128,59 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 909502395} m_Modifications: - - target: {fileID: 6852530814182375312, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375312, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_Name value: Cylinder objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_RootOrder value: 1 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalPosition.x value: 20 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalPosition.y value: 1 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalPosition.z value: 20 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalRotation.w value: -0.38268325 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalRotation.y value: 0.9238796 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalRotation.z value: -0 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 225 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375318, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375318, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: sceneId value: 4277306991 objectReference: {fileID: 0} - - target: {fileID: 6852530814182375318, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + - target: {fileID: 6852530814182375318, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} propertyPath: m_SceneId value: 568164022 objectReference: {fileID: 0} @@ -1127,8 +1188,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} --- !u!4 &901271863 stripped Transform: - m_CorrespondingSourceObject: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, - type: 3} + m_CorrespondingSourceObject: {fileID: 6852530814182375316, guid: 12a4c14e672c00b4b840f937d824b890, type: 3} m_PrefabInstance: {fileID: 901271862} m_PrefabAsset: {fileID: 0} --- !u!1 &909502394 @@ -1157,6 +1217,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 748207075} - {fileID: 901271863} @@ -1195,6 +1256,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -8, y: 3, z: -8} m_LocalScale: {x: 1, y: 3, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1608696205} m_RootOrder: 0 @@ -1224,10 +1286,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1252,6 +1316,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1047741294 MeshFilter: m_ObjectHideFlags: 0 @@ -1262,8 +1327,7 @@ MeshFilter: m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &1072006166 stripped GameObject: - m_CorrespondingSourceObject: {fileID: 160176457, guid: ab222ed73ada1ac4ba2f61e843d7627c, - type: 3} + m_CorrespondingSourceObject: {fileID: 160176457, guid: ab222ed73ada1ac4ba2f61e843d7627c, type: 3} m_PrefabInstance: {fileID: 160176455} m_PrefabAsset: {fileID: 0} --- !u!114 &1072006167 @@ -1278,6 +1342,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: b2e242ee38a14076a39934172a19079b, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0.1 visRange: 10 --- !u!1 &1172568541 GameObject: @@ -1305,6 +1372,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 178547538} - {fileID: 1816951100} @@ -1315,8 +1383,7 @@ Transform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!4 &1284471874 stripped Transform: - m_CorrespondingSourceObject: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + m_CorrespondingSourceObject: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} m_PrefabInstance: {fileID: 1076878375580925077} m_PrefabAsset: {fileID: 0} --- !u!1 &1405375878 @@ -1395,6 +1462,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &1405375880 @@ -1407,6 +1475,7 @@ Transform: m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} m_LocalPosition: {x: 0, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 1 @@ -1511,10 +1580,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1539,6 +1610,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1461518031 MeshFilter: m_ObjectHideFlags: 0 @@ -1557,6 +1629,7 @@ Transform: m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 50, y: 50, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 4 @@ -1590,6 +1663,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 8, y: 3, z: 8} m_LocalScale: {x: 1, y: 3, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1608696205} m_RootOrder: 2 @@ -1619,10 +1693,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1647,6 +1723,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1462312437 MeshFilter: m_ObjectHideFlags: 0 @@ -1655,6 +1732,74 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1462312433} m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &1471959939 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1471959942} + - component: {fileID: 1471959941} + - component: {fileID: 1471959940} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1471959940 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1471959939} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &1471959941 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1471959939} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1471959942 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1471959939} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 8 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1608696204 GameObject: m_ObjectHideFlags: 0 @@ -1681,6 +1826,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1047741291} - {fileID: 612284968} @@ -1719,10 +1865,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 0 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 0 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1747,6 +1895,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1630383478 MeshFilter: m_ObjectHideFlags: 0 @@ -1765,6 +1914,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 32, y: 32, z: 32} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 6 @@ -1802,23 +1952,21 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: dontDestroyOnLoad: 0 - PersistNetworkManagerToOfflineScene: 0 runInBackground: 1 autoStartServerBuild: 1 - serverTickRate: 30 + autoConnectClientBuild: 0 + sendRate: 30 offlineScene: onlineScene: transport: {fileID: 1661834280} networkAddress: localhost maxConnections: 100 - disconnectInactiveConnections: 0 - disconnectInactiveTimeout: 60 authenticator: {fileID: 0} - playerPrefab: {fileID: 8872462076811691049, guid: a5bdca0a2315d43499be7dcef473fbc7, - type: 3} + playerPrefab: {fileID: 8872462076811691049, guid: a5bdca0a2315d43499be7dcef473fbc7, type: 3} autoCreatePlayer: 1 playerSpawnMethod: 1 spawnPrefabs: [] + timeInterpolationGui: 0 Zone: {fileID: 3460729395543957449, guid: de939020b5e2aa5489ebcc4002d75d54, type: 3} subScenes: - Assets/Mirror/Examples/AdditiveScenes/Scenes/SubScene.unity @@ -1832,6 +1980,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 2 @@ -1854,10 +2003,12 @@ MonoBehaviour: Interval: 10 Timeout: 10000 FastResend: 2 - CongestionWindow: 0 SendWindowSize: 4096 ReceiveWindowSize: 4096 - NonAlloc: 1 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 298449 + UnreliableMaxMessageSize: 1199 debugLog: 0 statisticsGUI: 0 statisticsLog: 0 @@ -1873,7 +2024,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} m_Name: m_EditorClassIdentifier: - showGUI: 1 offsetX: 0 offsetY: 0 --- !u!114 &1661834282 @@ -1919,10 +2069,12 @@ MeshRenderer: m_CastShadows: 0 m_ReceiveShadows: 0 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1947,6 +2099,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1727677798 MeshFilter: m_ObjectHideFlags: 0 @@ -1965,6 +2118,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.92387956, z: 0, w: 0.38268343} m_LocalPosition: {x: -20, y: 1, z: 20} m_LocalScale: {x: 10, y: 10, z: 10} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 47225731} m_RootOrder: 0 @@ -1996,6 +2150,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} m_LocalPosition: {x: -20, y: 1.08, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1172568542} m_RootOrder: 1 @@ -2019,78 +2174,63 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 909502395} m_Modifications: - - target: {fileID: 1076878374699499732, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499732, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_Name value: Capsule objectReference: {fileID: 0} - - target: {fileID: 1076878374699499732, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499732, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_StaticEditorFlags value: 4294967295 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_RootOrder value: 3 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalPosition.x value: 20 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalPosition.y value: 1 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalPosition.z value: -20 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalRotation.w value: -0.92387944 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalRotation.y value: 0.3826836 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalRotation.z value: -0 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 315 objectReference: {fileID: 0} - - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 1076878374699499735, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 2648107611936813301, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 2648107611936813301, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: sceneId value: 2757245015 objectReference: {fileID: 0} - - target: {fileID: 2648107611936813301, guid: e1971f4a8c7661546bc509b44bd91b80, - type: 3} + - target: {fileID: 2648107611936813301, guid: e1971f4a8c7661546bc509b44bd91b80, type: 3} propertyPath: m_SceneId value: 2061538488 objectReference: {fileID: 0} diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scripts/AdditiveNetworkManager.cs b/Assets/Mirror/Examples/AdditiveScenes/Scripts/AdditiveNetworkManager.cs index 756a5dc..93e3d54 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scripts/AdditiveNetworkManager.cs +++ b/Assets/Mirror/Examples/AdditiveScenes/Scripts/AdditiveNetworkManager.cs @@ -2,11 +2,6 @@ using UnityEngine; using UnityEngine.SceneManagement; -/* - Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager - API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html -*/ - namespace Mirror.Examples.AdditiveScenes { [AddComponentMenu("")] @@ -19,6 +14,18 @@ public class AdditiveNetworkManager : NetworkManager [Tooltip("Add all sub-scenes to this list")] public string[] subScenes; + public static new AdditiveNetworkManager singleton { get; private set; } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + singleton = this; + } + public override void OnStartServer() { base.OnStartServer(); @@ -37,7 +44,8 @@ public override void OnStopServer() public override void OnStopClient() { - StartCoroutine(UnloadScenes()); + if (mode == NetworkManagerMode.Offline) + StartCoroutine(UnloadScenes()); } IEnumerator LoadSubScenes() @@ -45,10 +53,7 @@ IEnumerator LoadSubScenes() Debug.Log("Loading Scenes"); foreach (string sceneName in subScenes) - { yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); - // Debug.Log($"Loaded {sceneName}"); - } } IEnumerator UnloadScenes() @@ -57,10 +62,7 @@ IEnumerator UnloadScenes() foreach (string sceneName in subScenes) if (SceneManager.GetSceneByName(sceneName).IsValid() || SceneManager.GetSceneByPath(sceneName).IsValid()) - { yield return SceneManager.UnloadSceneAsync(sceneName); - // Debug.Log($"Unloaded {sceneName}"); - } yield return Resources.UnloadUnusedAssets(); } diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerCamera.cs b/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerCamera.cs index 95a52f5..2f9e511 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerCamera.cs +++ b/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerCamera.cs @@ -24,6 +24,8 @@ public override void OnStartLocalPlayer() mainCam.transform.localPosition = new Vector3(0f, 3f, -8f); mainCam.transform.localEulerAngles = new Vector3(10f, 0f, 0f); } + else + Debug.LogWarning("PlayerCamera: Could not find a camera in scene with 'MainCamera' tag."); } public override void OnStopLocalPlayer() @@ -33,6 +35,7 @@ public override void OnStopLocalPlayer() mainCam.transform.SetParent(null); SceneManager.MoveGameObjectToScene(mainCam.gameObject, SceneManager.GetActiveScene()); mainCam.orthographic = true; + mainCam.orthographicSize = 15f; mainCam.transform.localPosition = new Vector3(0f, 70f, 0f); mainCam.transform.localEulerAngles = new Vector3(90f, 0f, 0f); } diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerController.cs b/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerController.cs index f27163e..683a51e 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerController.cs +++ b/Assets/Mirror/Examples/AdditiveScenes/Scripts/PlayerController.cs @@ -1,4 +1,5 @@ using UnityEngine; +using UnityEngine.Networking; namespace Mirror.Examples.AdditiveScenes { @@ -8,88 +9,177 @@ namespace Mirror.Examples.AdditiveScenes [RequireComponent(typeof(Rigidbody))] public class PlayerController : NetworkBehaviour { + public enum GroundState : byte { Jumping, Falling, Grounded } + + [Header("Avatar Components")] public CharacterController characterController; + [Header("Movement")] + [Range(1, 20)] + public float moveSpeedMultiplier = 8f; + + [Header("Turning")] + [Range(1f, 200f)] + public float maxTurnSpeed = 100f; + [Range(.5f, 5f)] + public float turnDelta = 3f; + + [Header("Jumping")] + [Range(0.1f, 1f)] + public float initialJumpSpeed = 0.2f; + [Range(1f, 10f)] + public float maxJumpSpeed = 5f; + [Range(0.1f, 1f)] + public float jumpDelta = 0.2f; + + [Header("Diagnostics - Do Not Modify")] + public GroundState groundState = GroundState.Grounded; + + [Range(-1f, 1f)] + public float horizontal; + [Range(-1f, 1f)] + public float vertical; + + [Range(-200f, 200f)] + public float turnSpeed; + + [Range(-10f, 10f)] + public float jumpSpeed; + + [Range(-1.5f, 1.5f)] + public float animVelocity; + + [Range(-1.5f, 1.5f)] + public float animRotation; + + public Vector3Int velocity; + public Vector3 direction; + void OnValidate() { if (characterController == null) characterController = GetComponent(); + // Override CharacterController default values characterController.enabled = false; + characterController.skinWidth = 0.02f; + characterController.minMoveDistance = 0f; + GetComponent().isKinematic = true; - GetComponent().clientAuthority = true; + + this.enabled = false; } - public override void OnStartLocalPlayer() + public override void OnStartAuthority() { characterController.enabled = true; + this.enabled = true; } - [Header("Movement Settings")] - public float moveSpeed = 8f; - public float turnSensitivity = 5f; - public float maxTurnSpeed = 100f; - - [Header("Diagnostics")] - public float horizontal; - public float vertical; - public float turn; - public float jumpSpeed; - public bool isGrounded = true; - public bool isFalling; - public Vector3 velocity; + public override void OnStopAuthority() + { + this.enabled = false; + characterController.enabled = false; + } void Update() { - if (!isLocalPlayer || characterController == null || !characterController.enabled) + if (!characterController.enabled) return; - horizontal = Input.GetAxis("Horizontal"); - vertical = Input.GetAxis("Vertical"); + HandleTurning(); + HandleJumping(); + HandleMove(); - // Q and E cancel each other out, reducing the turn to zero + // Reset ground state + if (characterController.isGrounded) + groundState = GroundState.Grounded; + else if (groundState != GroundState.Jumping) + groundState = GroundState.Falling; + + // Diagnostic velocity...FloorToInt for display purposes + velocity = Vector3Int.FloorToInt(characterController.velocity); + } + + // TODO: Turning works while airborne...feature? + void HandleTurning() + { + // Q and E cancel each other out, reducing the turn to zero. if (Input.GetKey(KeyCode.Q)) - turn = Mathf.MoveTowards(turn, -maxTurnSpeed, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, -maxTurnSpeed, turnDelta); if (Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, maxTurnSpeed, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, maxTurnSpeed, turnDelta); + + // If both pressed, reduce turning speed toward zero. if (Input.GetKey(KeyCode.Q) && Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, 0, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, 0, turnDelta); + + // If neither pressed, reduce turning speed toward zero. if (!Input.GetKey(KeyCode.Q) && !Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, 0, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, 0, turnDelta); - if (isGrounded) - isFalling = false; + transform.Rotate(0f, turnSpeed * Time.deltaTime, 0f); + } - if ((isGrounded || !isFalling) && jumpSpeed < 1f && Input.GetKey(KeyCode.Space)) + void HandleJumping() + { + // Handle variable force jumping. + // Jump starts with initial power on takeoff, and jumps higher / longer + // as player holds spacebar. Jump power is increased by a diminishing amout + // every frame until it reaches maxJumpSpeed, or player releases the spacebar, + // and then changes to the falling state until it gets grounded. + if (groundState != GroundState.Falling && Input.GetKey(KeyCode.Space)) { - jumpSpeed = Mathf.Lerp(jumpSpeed, 1f, 0.5f); + if (groundState != GroundState.Jumping) + { + // Start jump at initial power. + groundState = GroundState.Jumping; + jumpSpeed = initialJumpSpeed; + } + else + // Jumping has already started...increase power toward maxJumpSpeed over time. + jumpSpeed = Mathf.MoveTowards(jumpSpeed, maxJumpSpeed, jumpDelta); + + // If power has reached maxJumpSpeed, change to falling until grounded. + // This prevents over-applying jump power while already in the air. + if (jumpSpeed == maxJumpSpeed) + groundState = GroundState.Falling; } - else if (!isGrounded) + else if (groundState != GroundState.Grounded) { - isFalling = true; - jumpSpeed = 0; + // handles running off a cliff and/or player released Spacebar. + groundState = GroundState.Falling; + jumpSpeed = Mathf.Min(jumpSpeed, maxJumpSpeed); + jumpSpeed += Physics.gravity.y * Time.deltaTime; } + else + jumpSpeed = Physics.gravity.y * Time.deltaTime; } - void FixedUpdate() + // TODO: Directional input works while airborne...feature? + void HandleMove() { - if (!isLocalPlayer || characterController == null || !characterController.enabled) - return; + // Capture inputs + horizontal = Input.GetAxis("Horizontal"); + vertical = Input.GetAxis("Vertical"); - transform.Rotate(0f, turn * Time.fixedDeltaTime, 0f); + // Create initial direction vector without jumpSpeed (y-axis). + direction = new Vector3(horizontal, 0f, vertical); - Vector3 direction = new Vector3(horizontal, jumpSpeed, vertical); + // Clamp so diagonal strafing isn't a speed advantage. direction = Vector3.ClampMagnitude(direction, 1f); + + // Transforms direction from local space to world space. direction = transform.TransformDirection(direction); - direction *= moveSpeed; - if (jumpSpeed > 0) - characterController.Move(direction * Time.fixedDeltaTime); - else - characterController.SimpleMove(direction); + // Multiply for desired ground speed. + direction *= moveSpeedMultiplier; + + // Add jumpSpeed to direction as last step. + direction.y = jumpSpeed; - isGrounded = characterController.isGrounded; - velocity = characterController.velocity; + // Finally move the character. + characterController.Move(direction * Time.deltaTime); } } } diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scripts/RandomColor.cs b/Assets/Mirror/Examples/AdditiveScenes/Scripts/RandomColor.cs index 42f1b58..a2d3f9a 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scripts/RandomColor.cs +++ b/Assets/Mirror/Examples/AdditiveScenes/Scripts/RandomColor.cs @@ -6,7 +6,6 @@ public class RandomColor : NetworkBehaviour { public override void OnStartServer() { - base.OnStartServer(); color = Random.ColorHSV(0f, 1f, 1f, 1f, 0.5f, 1f); } diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scripts/ShootingTankBehaviour.cs b/Assets/Mirror/Examples/AdditiveScenes/Scripts/ShootingTankBehaviour.cs index 4492324..0b49dfa 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scripts/ShootingTankBehaviour.cs +++ b/Assets/Mirror/Examples/AdditiveScenes/Scripts/ShootingTankBehaviour.cs @@ -36,7 +36,7 @@ void ShootNearestPlayer() GameObject target = null; float distance = 100f; - foreach (NetworkConnection networkConnection in netIdentity.observers.Values) + foreach (NetworkConnectionToClient networkConnection in netIdentity.observers.Values) { GameObject tempTarget = networkConnection.identity.gameObject; float tempDistance = Vector3.Distance(tempTarget.transform.position, transform.position); diff --git a/Assets/Mirror/Examples/AdditiveScenes/Scripts/ZoneHandler.cs b/Assets/Mirror/Examples/AdditiveScenes/Scripts/ZoneHandler.cs index 180b271..040eaa9 100644 --- a/Assets/Mirror/Examples/AdditiveScenes/Scripts/ZoneHandler.cs +++ b/Assets/Mirror/Examples/AdditiveScenes/Scripts/ZoneHandler.cs @@ -2,7 +2,6 @@ namespace Mirror.Examples.AdditiveScenes { - // This script is attached to a prefab called Zone that is on the Player layer // AdditiveNetworkManager, in OnStartServer, instantiates the prefab only on the server. // It never exists for clients (other than host client if there is one). // The prefab has a Sphere Collider with isTrigger = true. @@ -17,21 +16,27 @@ public class ZoneHandler : MonoBehaviour [ServerCallback] void OnTriggerEnter(Collider other) { - // Debug.Log($"Loading {subScene}"); + // ignore collisions with non-Player objects + if (!other.CompareTag("Player")) return; - NetworkIdentity networkIdentity = other.gameObject.GetComponent(); - SceneMessage message = new SceneMessage{ sceneName = subScene, sceneOperation = SceneOperation.LoadAdditive }; - networkIdentity.connectionToClient.Send(message); + if (other.TryGetComponent(out NetworkIdentity networkIdentity)) + { + SceneMessage message = new SceneMessage { sceneName = subScene, sceneOperation = SceneOperation.LoadAdditive }; + networkIdentity.connectionToClient.Send(message); + } } [ServerCallback] void OnTriggerExit(Collider other) { - // Debug.Log($"Unloading {subScene}"); + // ignore collisions with non-Player objects + if (!other.CompareTag("Player")) return; - NetworkIdentity networkIdentity = other.gameObject.GetComponent(); - SceneMessage message = new SceneMessage{ sceneName = subScene, sceneOperation = SceneOperation.UnloadAdditive }; - networkIdentity.connectionToClient.Send(message); + if (other.TryGetComponent(out NetworkIdentity networkIdentity)) + { + SceneMessage message = new SceneMessage { sceneName = subScene, sceneOperation = SceneOperation.UnloadAdditive }; + networkIdentity.connectionToClient.Send(message); + } } } } diff --git a/Assets/Mirror/Examples/Basic/Prefabs/Player.prefab b/Assets/Mirror/Examples/Basic/Prefabs/Player.prefab index b9a33ea..e7d482a 100644 --- a/Assets/Mirror/Examples/Basic/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/Basic/Prefabs/Player.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -45,9 +46,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 3332857900 serverOnly: 0 visible: 0 - m_AssetId: dc2c4328591bef748abb8df795c17202 hasSpawned: 0 --- !u!114 &8550999602067651493 MonoBehaviour: @@ -61,12 +62,12 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a472ac3ae1701d149861871cf416a46d, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 - playerUIPrefab: {fileID: 5152941909035078397, guid: 22f1fa3a0aff72b46a371f667bb4fb30, - type: 3} + playerUIPrefab: {fileID: 5152941909035078397, guid: 22f1fa3a0aff72b46a371f667bb4fb30, type: 3} playerNumber: 0 - playerData: 0 playerColor: serializedVersion: 2 rgba: 4294967295 + playerData: 0 diff --git a/Assets/Mirror/Examples/Basic/Prefabs/PlayerUI.prefab b/Assets/Mirror/Examples/Basic/Prefabs/PlayerUI.prefab index 2dda39c..1b21595 100644 --- a/Assets/Mirror/Examples/Basic/Prefabs/PlayerUI.prefab +++ b/Assets/Mirror/Examples/Basic/Prefabs/PlayerUI.prefab @@ -28,6 +28,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5939842025755258307} m_RootOrder: 1 @@ -60,6 +61,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -107,6 +109,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 290719538378481902} - {fileID: 7941985369064644521} @@ -141,6 +144,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 0} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -198,6 +202,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5939842025755258307} m_RootOrder: 0 @@ -230,6 +235,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: diff --git a/Assets/Mirror/Examples/Basic/Scenes/Example.unity b/Assets/Mirror/Examples/Basic/Scenes/Example.unity index 66697db..e0edd3c 100644 --- a/Assets/Mirror/Examples/Basic/Scenes/Example.unity +++ b/Assets/Mirror/Examples/Basic/Scenes/Example.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 0 m_GISettings: serializedVersion: 2 @@ -98,7 +98,7 @@ LightmapSettings: m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_UseShadowmask: 1 + m_LightingSettings: {fileID: 2034431047} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -118,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -175,8 +177,10 @@ MonoBehaviour: CongestionWindow: 0 SendWindowSize: 4096 ReceiveWindowSize: 4096 - NonAlloc: 0 - ReliableMaxMessageSize: 4811624 + MaxRetransmit: 40 + NonAlloc: 1 + MaximizeSendReceiveBuffersToOSLimit: 1 + ReliableMaxMessageSize: 298449 UnreliableMaxMessageSize: 1199 debugLog: 0 statisticsGUI: 0 @@ -196,7 +200,8 @@ MonoBehaviour: dontDestroyOnLoad: 0 runInBackground: 1 autoStartServerBuild: 1 - serverTickRate: 30 + autoConnectClientBuild: 0 + sendRate: 30 offlineScene: onlineScene: transport: {fileID: 249891955} @@ -208,6 +213,7 @@ MonoBehaviour: autoCreatePlayer: 1 playerSpawnMethod: 1 spawnPrefabs: [] + timeInterpolationGui: 0 --- !u!4 &249891957 Transform: m_ObjectHideFlags: 0 @@ -218,6 +224,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: -10, y: 4, z: 5} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 1 @@ -232,6 +239,7 @@ GameObject: m_Component: - component: {fileID: 288173827} - component: {fileID: 288173826} + - component: {fileID: 288173828} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -292,10 +300,26 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -1} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &288173828 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 288173824} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 150 + maxLogCount: 50 + hotKey: 293 --- !u!1 &379082678 GameObject: m_ObjectHideFlags: 0 @@ -326,6 +350,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 864730913} m_RootOrder: 0 @@ -382,6 +407,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 0.039215688} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -464,6 +490,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!223 &533055203 Canvas: m_ObjectHideFlags: 0 @@ -495,6 +522,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1712119861} m_Father: {fileID: 0} @@ -533,6 +561,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 379082679} m_Father: {fileID: 1712119861} @@ -558,6 +587,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -610,6 +640,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} m_Name: m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 m_HorizontalAxis: Horizontal m_VerticalAxis: Vertical m_SubmitButton: Submit @@ -642,6 +673,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 3 @@ -674,6 +706,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 864730913} m_Father: {fileID: 533055204} @@ -699,6 +732,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 0} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -721,3 +755,65 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1712119860} m_CullTransparentMesh: 0 +--- !u!850595691 &2034431047 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 4 + m_GIWorkflowMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_LightmapCompression: 3 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_PVRTiledBaking: 0 diff --git a/Assets/Mirror/Examples/Basic/Scripts/BasicNetManager.cs b/Assets/Mirror/Examples/Basic/Scripts/BasicNetManager.cs index 4da39b5..23b1d2f 100644 --- a/Assets/Mirror/Examples/Basic/Scripts/BasicNetManager.cs +++ b/Assets/Mirror/Examples/Basic/Scripts/BasicNetManager.cs @@ -1,15 +1,23 @@ +using Mirror.Examples.AdditiveScenes; using UnityEngine; -/* - Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager - API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html -*/ - namespace Mirror.Examples.Basic { [AddComponentMenu("")] public class BasicNetManager : NetworkManager { + public static new BasicNetManager singleton { get; private set; } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + singleton = this; + } + /// /// Called on the server when a client adds a new player with NetworkClient.AddPlayer. /// The default implementation for this function creates a new player object from the playerPrefab. diff --git a/Assets/Mirror/Examples/Basic/Scripts/CanvasUI.cs b/Assets/Mirror/Examples/Basic/Scripts/CanvasUI.cs index d5c8987..7e7adb9 100644 --- a/Assets/Mirror/Examples/Basic/Scripts/CanvasUI.cs +++ b/Assets/Mirror/Examples/Basic/Scripts/CanvasUI.cs @@ -10,12 +10,19 @@ public class CanvasUI : MonoBehaviour [Tooltip("Assign Players Panel for instantiating PlayerUI as child")] public RectTransform playersPanel; - // static instance that can be referenced directly from Player script - public static CanvasUI instance; + // static instance that can be referenced from static methods below. + static CanvasUI instance; void Awake() { instance = this; } + + public static void SetActive(bool active) + { + instance.mainPanel.gameObject.SetActive(active); + } + + public static RectTransform GetPlayersPanel() => instance.playersPanel; } } diff --git a/Assets/Mirror/Examples/Basic/Scripts/Player.cs b/Assets/Mirror/Examples/Basic/Scripts/Player.cs index f62bc69..0261fce 100644 --- a/Assets/Mirror/Examples/Basic/Scripts/Player.cs +++ b/Assets/Mirror/Examples/Basic/Scripts/Player.cs @@ -122,10 +122,8 @@ public override void OnStopServer() /// public override void OnStartClient() { - Debug.Log("OnStartClient"); - // Instantiate the player UI as child of the Players Panel - playerUIObject = Instantiate(playerUIPrefab, CanvasUI.instance.playersPanel); + playerUIObject = Instantiate(playerUIPrefab, CanvasUI.GetPlayersPanel()); playerUI = playerUIObject.GetComponent(); // wire up all events to handlers in PlayerUI @@ -145,13 +143,11 @@ public override void OnStartClient() /// public override void OnStartLocalPlayer() { - Debug.Log("OnStartLocalPlayer"); - // Set isLocalPlayer for this Player in UI for background shading playerUI.SetLocalPlayer(); // Activate the main panel - CanvasUI.instance.mainPanel.gameObject.SetActive(true); + CanvasUI.SetActive(true); } /// @@ -161,7 +157,7 @@ public override void OnStartLocalPlayer() public override void OnStopLocalPlayer() { // Disable the main panel for local player - CanvasUI.instance.mainPanel.gameObject.SetActive(false); + CanvasUI.SetActive(false); } /// diff --git a/Assets/Mirror/Examples/Benchmark/Prefabs/Monster.prefab b/Assets/Mirror/Examples/Benchmark/Prefabs/Monster.prefab index 3625c3d..2617fa1 100644 --- a/Assets/Mirror/Examples/Benchmark/Prefabs/Monster.prefab +++ b/Assets/Mirror/Examples/Benchmark/Prefabs/Monster.prefab @@ -31,6 +31,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -54,10 +55,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -82,6 +85,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!114 &1078519278818213949 MonoBehaviour: m_ObjectHideFlags: 0 @@ -95,9 +99,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 3853995854 serverOnly: 0 visible: 0 - m_AssetId: 30b8f251d03d84284b70601e691d474f hasSpawned: 0 --- !u!114 &3679374677650722848 MonoBehaviour: @@ -111,23 +115,22 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 - syncInterval: 0.1 + syncInterval: 0 + target: {fileID: 2697352357490696306} clientAuthority: 0 - sendInterval: 0.05 syncPosition: 1 syncRotation: 0 syncScale: 0 - interpolatePosition: 1 - interpolateRotation: 0 - interpolateScale: 0 - bufferTimeMultiplier: 1 - bufferSizeLimit: 64 - catchupThreshold: 4 - catchupMultiplier: 0.1 showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + onlySyncOnChange: 0 + bufferResetMultiplier: 5 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 --- !u!114 &8309506939003697769 MonoBehaviour: m_ObjectHideFlags: 0 @@ -140,6 +143,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9cddc2e496c474e538a494465be0192a, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 speed: 1 diff --git a/Assets/Mirror/Examples/Benchmark/Prefabs/Player.prefab b/Assets/Mirror/Examples/Benchmark/Prefabs/Player.prefab index a7e4fdd..c2aaed7 100644 --- a/Assets/Mirror/Examples/Benchmark/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/Benchmark/Prefabs/Player.prefab @@ -31,6 +31,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -54,10 +55,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -82,6 +85,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!114 &1078519278818213949 MonoBehaviour: m_ObjectHideFlags: 0 @@ -95,9 +99,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 242745736 serverOnly: 0 visible: 0 - m_AssetId: e1299008405d14b17b1ca459a6cd44a2 hasSpawned: 0 --- !u!114 &3679374677650722848 MonoBehaviour: @@ -111,23 +115,22 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 1 syncMode: 0 - syncInterval: 0.1 - clientAuthority: 1 - sendInterval: 0.05 + syncInterval: 0 + target: {fileID: 2697352357490696306} + clientAuthority: 0 syncPosition: 1 syncRotation: 0 syncScale: 0 - interpolatePosition: 1 - interpolateRotation: 0 - interpolateScale: 0 - bufferTimeMultiplier: 1 - bufferSizeLimit: 64 - catchupThreshold: 4 - catchupMultiplier: 0.1 - showGizmos: 0 - showOverlay: 0 + showGizmos: 1 + showOverlay: 1 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + onlySyncOnChange: 0 + bufferResetMultiplier: 5 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 --- !u!114 &644305951047116972 MonoBehaviour: m_ObjectHideFlags: 0 @@ -140,6 +143,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: c482338c8cc6d4a3cba81934c0151972, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 speed: 20 diff --git a/Assets/Mirror/Examples/Benchmark/Scenes/Scene.unity b/Assets/Mirror/Examples/Benchmark/Scenes/Scene.unity index e29e9c8..b9e8b71 100644 --- a/Assets/Mirror/Examples/Benchmark/Scenes/Scene.unity +++ b/Assets/Mirror/Examples/Benchmark/Scenes/Scene.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 1 m_GISettings: serializedVersion: 2 @@ -132,6 +132,7 @@ GameObject: - component: {fileID: 88936777} - component: {fileID: 88936776} - component: {fileID: 88936774} + - component: {fileID: 88936778} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -207,10 +208,25 @@ Transform: m_LocalRotation: {x: 0.38268343, y: 0, z: 0, w: 0.92387956} m_LocalPosition: {x: 0, y: 50, z: -80} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 45, y: 0, z: 0} +--- !u!114 &88936778 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6635375fbc6be456ea640b75add6378e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + showLog: 0 --- !u!1 &535739935 GameObject: m_ObjectHideFlags: 0 @@ -331,6 +347,7 @@ MonoBehaviour: playerSpawnMethod: 1 spawnPrefabs: - {fileID: 449802645721213856, guid: 30b8f251d03d84284b70601e691d474f, type: 3} + timeInterpolationGui: 1 spawnPrefab: {fileID: 449802645721213856, guid: 30b8f251d03d84284b70601e691d474f, type: 3} spawnAmount: 1000 @@ -372,7 +389,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 39adc6e09d5544ed955a50ce8600355a, type: 3} m_Name: m_EditorClassIdentifier: - visRange: 200 + visRange: 30 rebuildInterval: 1 checkMethod: 0 showSlider: 1 diff --git a/Assets/Mirror/Examples/Benchmark/Scripts/BenchmarkNetworkManager.cs b/Assets/Mirror/Examples/Benchmark/Scripts/BenchmarkNetworkManager.cs index ba54712..786369f 100644 --- a/Assets/Mirror/Examples/Benchmark/Scripts/BenchmarkNetworkManager.cs +++ b/Assets/Mirror/Examples/Benchmark/Scripts/BenchmarkNetworkManager.cs @@ -2,6 +2,7 @@ namespace Mirror.Examples.Benchmark { + [AddComponentMenu("")] public class BenchmarkNetworkManager : NetworkManager { [Header("Spawns")] diff --git a/Assets/Mirror/Examples/Benchmark/Scripts/MonsterMovement.cs b/Assets/Mirror/Examples/Benchmark/Scripts/MonsterMovement.cs index 2ef7fae..fd7d377 100644 --- a/Assets/Mirror/Examples/Benchmark/Scripts/MonsterMovement.cs +++ b/Assets/Mirror/Examples/Benchmark/Scripts/MonsterMovement.cs @@ -38,15 +38,11 @@ void Update() { Vector2 circlePos = Random.insideUnitCircle; Vector3 dir = new Vector3(circlePos.x, 0, circlePos.y); - Vector3 dest = transform.position + dir * movementDistance; - // within move dist around start? + // set destination on random pos in a circle around start. // (don't want to wander off) - if (Vector3.Distance(start, dest) <= movementDistance) - { - destination = dest; - moving = true; - } + destination = start + dir * movementDistance; + moving = true; } } } diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta b/Assets/Mirror/Examples/CCU.meta similarity index 77% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta rename to Assets/Mirror/Examples/CCU.meta index 6878ad8..667a616 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta +++ b/Assets/Mirror/Examples/CCU.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6b7f3f8e8fa16475bbe48a8e9fbe800b +guid: c1224ecd334304d359f5930d0e7d85fc folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Examples/CCU/CCU.unity b/Assets/Mirror/Examples/CCU/CCU.unity new file mode 100644 index 0000000..a04b0bf --- /dev/null +++ b/Assets/Mirror/Examples/CCU/CCU.unity @@ -0,0 +1,527 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 0 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 23800000, guid: 0bc607fa2e315482ebe98797e844e11f, type: 2} +--- !u!1 &88936773 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 88936777} + - component: {fileID: 88936776} + - component: {fileID: 88936774} + - component: {fileID: 88936778} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &88936774 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 150 + maxLogCount: 50 + hotKey: 293 +--- !u!20 &88936776 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 35 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &88936777 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_LocalRotation: {x: 0.38268343, y: 0, z: 0, w: 0.92387956} + m_LocalPosition: {x: 0, y: 600, z: -800} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 45, y: 0, z: 0} +--- !u!114 &88936778 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6635375fbc6be456ea640b75add6378e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + showLog: 0 +--- !u!1 &535739935 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 535739936} + - component: {fileID: 535739937} + m_Layer: 0 + m_Name: SpawnPosition + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &535739936 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &535739937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1282001517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1282001518} + - component: {fileID: 1282001520} + - component: {fileID: 1282001519} + - component: {fileID: 1282001521} + - component: {fileID: 1282001522} + - component: {fileID: 1282001523} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1282001518 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1282001519 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!114 &1282001520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b0ff29cd45bcd43128c4cc79f25fd658, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 1 + runInBackground: 1 + autoStartServerBuild: 1 + autoConnectClientBuild: 0 + sendRate: 30 + offlineScene: + onlineScene: + transport: {fileID: 1282001521} + networkAddress: localhost + maxConnections: 1000 + authenticator: {fileID: 0} + playerPrefab: {fileID: 449802645721213856, guid: 614e28b6213c14195b8661c153bf4ee4, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 1 + spawnPrefabs: + - {fileID: 449802645721213856, guid: d0f3b049f6bef4cc6930c57a2146ca52, type: 3} + timeInterpolationGui: 0 + spawnAmount: 10000 + interleave: 10 + spawnPrefab: {fileID: 449802645721213856, guid: d0f3b049f6bef4cc6930c57a2146ca52, + type: 3} + spawnPositionRatio: 0.01 +--- !u!114 &1282001521 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b0fecffa3f624585964b0d0eb21b18e, type: 3} + m_Name: + m_EditorClassIdentifier: + Port: 7777 + DualMode: 1 + NoDelay: 1 + Interval: 10 + Timeout: 10000 + FastResend: 2 + CongestionWindow: 0 + SendWindowSize: 4096 + ReceiveWindowSize: 4096 + MaxRetransmit: 40 + NonAlloc: 1 + MaximizeSendReceiveBuffersToOSLimit: 1 + ReliableMaxMessageSize: 298449 + UnreliableMaxMessageSize: 1199 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!114 &1282001522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 39adc6e09d5544ed955a50ce8600355a, type: 3} + m_Name: + m_EditorClassIdentifier: + visRange: 30 + rebuildInterval: 1 + checkMethod: 0 + showSlider: 0 +--- !u!114 &1282001523 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6d7da4e566d24ea7b0e12178d934b648, type: 3} + m_Name: + m_EditorClassIdentifier: + clientIntervalReceivedPackets: 0 + clientIntervalReceivedBytes: 0 + clientIntervalSentPackets: 0 + clientIntervalSentBytes: 0 + clientReceivedPacketsPerSecond: 0 + clientReceivedBytesPerSecond: 0 + clientSentPacketsPerSecond: 0 + clientSentBytesPerSecond: 0 + serverIntervalReceivedPackets: 0 + serverIntervalReceivedBytes: 0 + serverIntervalSentPackets: 0 + serverIntervalSentBytes: 0 + serverReceivedPacketsPerSecond: 0 + serverReceivedBytesPerSecond: 0 + serverSentPacketsPerSecond: 0 + serverSentBytesPerSecond: 0 +--- !u!1 &2054208274 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054208276} + - component: {fileID: 2054208275} + m_Layer: 0 + m_Name: Directional light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2054208275 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 0.8 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &2054208276 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta b/Assets/Mirror/Examples/CCU/CCU.unity.meta similarity index 74% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta rename to Assets/Mirror/Examples/CCU/CCU.unity.meta index 67ab688..93790ee 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta +++ b/Assets/Mirror/Examples/CCU/CCU.unity.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f1256cadc037546ccb66071784fce137 +guid: a692de27a26e74566b797df05d9b3385 DefaultImporter: externalObjects: {} userData: diff --git a/Assets/Mirror/Examples/CCU/CCUNetworkManager.cs b/Assets/Mirror/Examples/CCU/CCUNetworkManager.cs new file mode 100644 index 0000000..3891eee --- /dev/null +++ b/Assets/Mirror/Examples/CCU/CCUNetworkManager.cs @@ -0,0 +1,93 @@ +using UnityEngine; + +namespace Mirror.Examples.CCU +{ + [AddComponentMenu("")] + public class CCUNetworkManager : NetworkManager + { + [Header("Spawns")] + public int spawnAmount = 10_000; + public float interleave = 1; + public GameObject spawnPrefab; + + // player spawn positions should be spread across the world. + // not all at one place. + // but _some_ at the same place. + // => deterministic random is ideal + [Range(0, 1)] public float spawnPositionRatio = 0.01f; + + System.Random random = new System.Random(42); + + void SpawnAll() + { + // clear previous player spawn positions in case we start twice + foreach (Transform position in startPositions) + Destroy(position.gameObject); + + startPositions.Clear(); + + // calculate sqrt so we can spawn N * N = Amount + float sqrt = Mathf.Sqrt(spawnAmount); + + // calculate spawn xz start positions + // based on spawnAmount * distance + float offset = -sqrt / 2 * interleave; + + // spawn exactly the amount, not one more. + int spawned = 0; + for (int spawnX = 0; spawnX < sqrt; ++spawnX) + { + for (int spawnZ = 0; spawnZ < sqrt; ++spawnZ) + { + // spawn exactly the amount, not any more + // (our sqrt method isn't 100% precise) + if (spawned < spawnAmount) + { + // spawn & position + GameObject go = Instantiate(spawnPrefab); + float x = offset + spawnX * interleave; + float z = offset + spawnZ * interleave; + Vector3 position = new Vector3(x, 0, z); + go.transform.position = position; + + // spawn + NetworkServer.Spawn(go); + ++spawned; + + // add random spawn position for players. + // don't have them all in the same place. + if (random.NextDouble() <= spawnPositionRatio) + { + GameObject spawnGO = new GameObject("Spawn"); + spawnGO.transform.position = position; + spawnGO.AddComponent(); + } + } + } + } + } + + // overwrite random spawn position selection: + // - needs to be deterministic so every CCU test results in the same + // - needs to be random so not only are the spawn positions spread out + // randomly, we also have a random amount of players per spawn position + public override Transform GetStartPosition() + { + // first remove any dead transforms + startPositions.RemoveAll(t => t == null); + + if (startPositions.Count == 0) + return null; + + // pick a random one + int index = random.Next(0, startPositions.Count); // DETERMINISTIC + return startPositions[index]; + } + + public override void OnStartServer() + { + base.OnStartServer(); + SpawnAll(); + } + } +} diff --git a/Assets/Mirror/Examples/CCU/CCUNetworkManager.cs.meta b/Assets/Mirror/Examples/CCU/CCUNetworkManager.cs.meta new file mode 100644 index 0000000..120e6c9 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/CCUNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b0ff29cd45bcd43128c4cc79f25fd658 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/CCU/Monster.cs b/Assets/Mirror/Examples/CCU/Monster.cs new file mode 100644 index 0000000..420c487 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Monster.cs @@ -0,0 +1,55 @@ +using UnityEngine; + +namespace Mirror.Examples.CCU +{ + public class Monster : NetworkBehaviour + { + public float speed = 1; + public float movementProbability = 0.5f; + public float movementDistance = 20; + + bool moving; + Vector3 start; + Vector3 destination; + + // cache .transform for benchmark demo. + // Component.get_transform shows in profiler otherwise. + Transform tf; + + public override void OnStartServer() + { + tf = transform; + start = tf.position; + } + + [ServerCallback] + void Update() + { + if (moving) + { + if (Vector3.Distance(tf.position, destination) <= 0.01f) + { + moving = false; + } + else + { + tf.position = Vector3.MoveTowards(tf.position, destination, speed * Time.deltaTime); + } + } + else + { + float r = Random.value; + if (r < movementProbability * Time.deltaTime) + { + Vector2 circlePos = Random.insideUnitCircle; + Vector3 dir = new Vector3(circlePos.x, 0, circlePos.y); + + // set destination on random pos in a circle around start. + // (don't want to wander off) + destination = start + dir * movementDistance; + moving = true; + } + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta b/Assets/Mirror/Examples/CCU/Monster.cs.meta similarity index 83% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta rename to Assets/Mirror/Examples/CCU/Monster.cs.meta index ef424ba..f8b4b6e 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta +++ b/Assets/Mirror/Examples/CCU/Monster.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: af0279d15e39b484792394f1d3cad4d9 +guid: b3a6fbf45e1c94ce6b2c1a2d08519a11 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Mirror/Examples/CCU/Monster.prefab b/Assets/Mirror/Examples/CCU/Monster.prefab new file mode 100644 index 0000000..7d56738 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Monster.prefab @@ -0,0 +1,151 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &449802645721213856 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2697352357490696306} + - component: {fileID: 8695142844114820487} + - component: {fileID: 6692327599609520039} + - component: {fileID: 1078519278818213949} + - component: {fileID: 3679374677650722848} + - component: {fileID: -3181616459286004871} + m_Layer: 0 + m_Name: Monster + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2697352357490696306 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &8695142844114820487 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &6692327599609520039 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 451c5da2c5056496297cffba02216286, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &1078519278818213949 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 204484799 + serverOnly: 0 + visible: 0 + hasSpawned: 0 +--- !u!114 &3679374677650722848 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + target: {fileID: 2697352357490696306} + clientAuthority: 0 + syncPosition: 1 + syncRotation: 0 + syncScale: 0 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + onlySyncOnChange: 1 + bufferResetMultiplier: 5 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 +--- !u!114 &-3181616459286004871 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b3a6fbf45e1c94ce6b2c1a2d08519a11, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0.1 + speed: 1 + movementProbability: 0.5 + movementDistance: 20 diff --git a/Assets/Mirror/Examples/CCU/Monster.prefab.meta b/Assets/Mirror/Examples/CCU/Monster.prefab.meta new file mode 100644 index 0000000..a949771 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Monster.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d0f3b049f6bef4cc6930c57a2146ca52 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/CCU/Player.cs b/Assets/Mirror/Examples/CCU/Player.cs new file mode 100644 index 0000000..dd98ca8 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Player.cs @@ -0,0 +1,99 @@ +using UnityEngine; + +namespace Mirror.Examples.CCU +{ + public class Player : NetworkBehaviour + { + public Vector3 cameraOffset = new Vector3(0, 40, -40); + + // automated movement. + // player may switch to manual movement any time + [Header("Automated Movement")] + public bool autoMove = true; + public float autoSpeed = 2; + public float movementProbability = 0.5f; + public float movementDistance = 20; + bool moving; + Vector3 start; + Vector3 destination; + + [Header("Manual Movement")] + public float manualSpeed = 10; + + // cache .transform for benchmark demo. + // Component.get_transform shows in profiler otherwise. + Transform tf; + + public override void OnStartLocalPlayer() + { + tf = transform; + start = tf.position; + + // make camera follow + Camera.main.transform.SetParent(transform, false); + Camera.main.transform.localPosition = cameraOffset; + } + + public override void OnStopLocalPlayer() + { + // free the camera so we don't destroy it too + Camera.main.transform.SetParent(null, true); + } + + void AutoMove() + { + if (moving) + { + if (Vector3.Distance(tf.position, destination) <= 0.01f) + { + moving = false; + } + else + { + tf.position = Vector3.MoveTowards(tf.position, destination, autoSpeed * Time.deltaTime); + } + } + else + { + float r = Random.value; + if (r < movementProbability * Time.deltaTime) + { + // calculate a random position in a circle + float circleX = Mathf.Cos(Random.value * Mathf.PI); + float circleZ = Mathf.Sin(Random.value * Mathf.PI); + Vector2 circlePos = new Vector2(circleX, circleZ); + Vector3 dir = new Vector3(circlePos.x, 0, circlePos.y); + + // set destination on random pos in a circle around start. + // (don't want to wander off) + destination = start + dir * movementDistance; + moving = true; + } + } + } + + void ManualMove() + { + float h = Input.GetAxis("Horizontal"); + float v = Input.GetAxis("Vertical"); + + Vector3 direction = new Vector3(h, 0, v); + transform.position += direction.normalized * (Time.deltaTime * manualSpeed); + } + + static bool Interrupted() => + Input.GetAxis("Horizontal") != 0 || Input.GetAxis("Vertical") != 0; + + void Update() + { + if (!isLocalPlayer) return; + + // player may interrupt auto movement to switch to manual + if (Interrupted()) autoMove = false; + + // move + if (autoMove) AutoMove(); + else ManualMove(); + } + } +} diff --git a/Assets/Mirror/Examples/CCU/Player.cs.meta b/Assets/Mirror/Examples/CCU/Player.cs.meta new file mode 100644 index 0000000..792483e --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Player.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c936c3d1cc664f15a92190fc35fd7dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/CCU/Player.prefab b/Assets/Mirror/Examples/CCU/Player.prefab new file mode 100644 index 0000000..67a4648 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Player.prefab @@ -0,0 +1,174 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &449802645721213856 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2697352357490696306} + - component: {fileID: 8695142844114820487} + - component: {fileID: 6692327599609520039} + - component: {fileID: 1078519278818213949} + - component: {fileID: 3679374677650722848} + - component: {fileID: 8691745481282286165} + - component: {fileID: 8079286830074380037} + m_Layer: 0 + m_Name: Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2697352357490696306 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &8695142844114820487 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &6692327599609520039 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: ff8f106e5c9e34da28ad9cee6edb2255, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &1078519278818213949 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 1350054018 + serverOnly: 0 + visible: 0 + hasSpawned: 0 +--- !u!114 &3679374677650722848 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 1 + syncMode: 0 + syncInterval: 0 + target: {fileID: 2697352357490696306} + clientAuthority: 0 + syncPosition: 1 + syncRotation: 0 + syncScale: 0 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + onlySyncOnChange: 1 + bufferResetMultiplier: 5 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 +--- !u!114 &8691745481282286165 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2c936c3d1cc664f15a92190fc35fd7dd, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0.1 + cameraOffset: {x: 0, y: 40, z: -40} + autoMove: 1 + autoSpeed: 2 + movementProbability: 0.5 + movementDistance: 20 + manualSpeed: 10 +--- !u!114 &8079286830074380037 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 449802645721213856} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ba360e4ff6b44fc6898f56322b90c6c8, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 1 + syncInterval: 0.1 + sendInterval: 1 + showGui: 0 + hotKey: 292 + passwordFile: remote_statistics.txt diff --git a/Assets/Mirror/Examples/CCU/Player.prefab.meta b/Assets/Mirror/Examples/CCU/Player.prefab.meta new file mode 100644 index 0000000..1fd32ad --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Player.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 614e28b6213c14195b8661c153bf4ee4 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/CCU/Red.mat b/Assets/Mirror/Examples/CCU/Red.mat new file mode 100644 index 0000000..5c24968 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Red.mat @@ -0,0 +1,77 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Red + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: _EMISSION + m_LightmapFlags: 0 + m_EnableInstancingVariants: 1 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0.3254902, g: 0.3254902, b: 0.3254902, a: 1} diff --git a/Assets/Mirror/Examples/CCU/Red.mat.meta b/Assets/Mirror/Examples/CCU/Red.mat.meta new file mode 100644 index 0000000..51eef59 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/Red.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b742246ea1d249358a8a8e039bd4b17 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/CCU/White.mat b/Assets/Mirror/Examples/CCU/White.mat new file mode 100644 index 0000000..aaf3087 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/White.mat @@ -0,0 +1,77 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: White + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: _EMISSION + m_LightmapFlags: 0 + m_EnableInstancingVariants: 1 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0.3254902, g: 0.3254902, b: 0.3254902, a: 1} diff --git a/Assets/Mirror/Examples/CCU/White.mat.meta b/Assets/Mirror/Examples/CCU/White.mat.meta new file mode 100644 index 0000000..f673848 --- /dev/null +++ b/Assets/Mirror/Examples/CCU/White.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ae32f0609a5e9453b95ec3b9e28c2ac8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/Chat/Prefabs/Player.prefab b/Assets/Mirror/Examples/Chat/Prefabs/Player.prefab index c4fb98f..5e910fa 100644 --- a/Assets/Mirror/Examples/Chat/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/Chat/Prefabs/Player.prefab @@ -9,8 +9,8 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 3351063249001228125} - - component: {fileID: 718303009120396421} - component: {fileID: 114398755512196590} + - component: {fileID: 718303009120396421} m_Layer: 0 m_Name: Player m_TagString: Untagged @@ -28,11 +28,12 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 718.76324, y: 411.311, z: -4.8041315} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &718303009120396421 +--- !u!114 &114398755512196590 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -41,13 +42,15 @@ MonoBehaviour: m_GameObject: {fileID: 5075528875289742095} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 3addc5ad220944ed6888319897606739, type: 3} + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} m_Name: m_EditorClassIdentifier: - syncMode: 0 - syncInterval: 0.1 - playerName: ---- !u!114 &114398755512196590 + sceneId: 0 + _assetId: 725701738 + serverOnly: 0 + visible: 0 + hasSpawned: 0 +--- !u!114 &718303009120396421 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -56,9 +59,10 @@ MonoBehaviour: m_GameObject: {fileID: 5075528875289742095} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Script: {fileID: 11500000, guid: 3addc5ad220944ed6888319897606739, type: 3} m_Name: m_EditorClassIdentifier: - serverOnly: 0 - m_AssetId: - m_SceneId: 0 + syncDirection: 0 + syncMode: 0 + syncInterval: 0.1 + playerName: diff --git a/Assets/Mirror/Examples/Chat/Scenes/Main.unity b/Assets/Mirror/Examples/Chat/Scenes/Main.unity index ef20276..b138484 100644 --- a/Assets/Mirror/Examples/Chat/Scenes/Main.unity +++ b/Assets/Mirror/Examples/Chat/Scenes/Main.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 0 m_GISettings: serializedVersion: 2 @@ -98,7 +98,7 @@ LightmapSettings: m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_UseShadowmask: 1 + m_LightingSettings: {fileID: 212571282} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -118,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -171,8 +173,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.39215687} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -212,7 +215,7 @@ GameObject: - component: {fileID: 75860998} - component: {fileID: 75860997} m_Layer: 5 - m_Name: Chat + m_Name: ChatPanel m_TagString: ChatWindow m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -230,9 +233,10 @@ RectTransform: m_LocalScale: {x: 1, y: 1, z: 1} m_Children: - {fileID: 762534976} + - {fileID: 1731300362} + - {fileID: 1863915625} - {fileID: 1231350850} - {fileID: 1286463573} - - {fileID: 1863915625} m_Father: {fileID: 719573003} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -271,6 +275,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0.92941177} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -345,6 +350,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -421,8 +427,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_Color: {r: 0, g: 0.5019608, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -449,6 +456,67 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 107824418} m_CullTransparentMesh: 0 +--- !u!850595691 &212571282 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 3 + m_GIWorkflowMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_TextureCompression: 1 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 --- !u!1 &423302019 GameObject: m_ObjectHideFlags: 0 @@ -502,6 +570,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -551,6 +620,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -625,6 +695,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -703,6 +774,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -729,6 +801,85 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 591385423} m_CullTransparentMesh: 0 +--- !u!1 &637644698 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 637644699} + - component: {fileID: 637644701} + - component: {fileID: 637644700} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &637644699 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 637644698} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1731300362} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &637644700 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 637644698} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 18 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: X +--- !u!222 &637644701 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 637644698} + m_CullTransparentMesh: 1 --- !u!1 &719572997 GameObject: m_ObjectHideFlags: 0 @@ -749,7 +900,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 0 + m_IsActive: 1 --- !u!114 &719572998 MonoBehaviour: m_ObjectHideFlags: 0 @@ -764,10 +915,10 @@ MonoBehaviour: m_EditorClassIdentifier: syncMode: 0 syncInterval: 0.1 - chatMessage: {fileID: 1231350851} chatHistory: {fileID: 827598817} scrollbar: {fileID: 423302021} - localPlayerName: + chatMessage: {fileID: 1231350851} + sendButton: {fileID: 1286463574} --- !u!114 &719572999 MonoBehaviour: m_ObjectHideFlags: 0 @@ -824,6 +975,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!223 &719573002 Canvas: m_ObjectHideFlags: 0 @@ -915,8 +1067,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.39215687} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -934,7 +1087,7 @@ MonoBehaviour: m_HorizontalOverflow: 0 m_VerticalOverflow: 0 m_LineSpacing: 1 - m_Text: Enter text... + m_Text: Enter text and press Enter or click Send... --- !u!222 &719610388 CanvasRenderer: m_ObjectHideFlags: 0 @@ -1014,7 +1167,7 @@ RectTransform: m_AnchorMin: {x: 0.5, y: 1} m_AnchorMax: {x: 0.5, y: 1} m_AnchoredPosition: {x: 0, y: -30} - m_SizeDelta: {x: 300, y: 40} + m_SizeDelta: {x: 1211, y: 40} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &762534977 MonoBehaviour: @@ -1029,8 +1182,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_Color: {r: 0, g: 0, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1038,11 +1192,11 @@ MonoBehaviour: m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 30 - m_FontStyle: 0 + m_FontStyle: 1 m_BestFit: 0 m_MinSize: 3 m_MaxSize: 40 - m_Alignment: 1 + m_Alignment: 4 m_AlignByGeometry: 0 m_RichText: 1 m_HorizontalOverflow: 0 @@ -1092,9 +1246,9 @@ RectTransform: m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: -17, y: 0} + m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 1} --- !u!114 &780870087 MonoBehaviour: @@ -1111,6 +1265,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1178,10 +1333,10 @@ RectTransform: m_Father: {fileID: 1335915325} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 556, y: -5} - m_SizeDelta: {x: 1102, y: 22} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 1} --- !u!114 &827598817 MonoBehaviour: @@ -1198,6 +1353,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1278,6 +1434,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -1321,6 +1478,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1783103024} + m_TargetAssemblyTypeName: m_MethodName: SetPlayername m_Mode: 0 m_Arguments: @@ -1332,6 +1490,7 @@ MonoBehaviour: m_BoolArgument: 0 m_CallState: 2 - m_Target: {fileID: 1453327789} + m_TargetAssemblyTypeName: m_MethodName: ToggleButtons m_Mode: 0 m_Arguments: @@ -1365,6 +1524,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1437,8 +1597,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_Color: {r: 0, g: 0.5019608, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1521,6 +1682,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -1564,6 +1726,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1783103025} + m_TargetAssemblyTypeName: m_MethodName: SetHostname m_Mode: 0 m_Arguments: @@ -1597,6 +1760,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1672,6 +1836,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -1702,6 +1867,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1453327784} + m_TargetAssemblyTypeName: m_MethodName: SetActive m_Mode: 6 m_Arguments: @@ -1713,6 +1879,7 @@ MonoBehaviour: m_BoolArgument: 0 m_CallState: 2 - m_Target: {fileID: 1783103025} + m_TargetAssemblyTypeName: m_MethodName: StartClient m_Mode: 1 m_Arguments: @@ -1738,6 +1905,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1810,8 +1978,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.39215687} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -1871,7 +2040,7 @@ RectTransform: - {fileID: 719610386} - {fileID: 90143747} m_Father: {fileID: 75860996} - m_RootOrder: 1 + m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 0} @@ -1892,6 +2061,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -1932,6 +2102,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 719572998} + m_TargetAssemblyTypeName: m_MethodName: OnEndEdit m_Mode: 0 m_Arguments: @@ -1944,7 +2115,19 @@ MonoBehaviour: m_CallState: 2 m_OnValueChanged: m_PersistentCalls: - m_Calls: [] + m_Calls: + - m_Target: {fileID: 719572998} + m_TargetAssemblyTypeName: Mirror.Examples.Chat.ChatUI, Mirror.Examples + m_MethodName: ToggleButton + m_Mode: 0 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_CustomCaretColor: 0 m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} @@ -1968,6 +2151,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2022,7 +2206,7 @@ RectTransform: m_Children: - {fileID: 1018203014} m_Father: {fileID: 75860996} - m_RootOrder: 2 + m_RootOrder: 4 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 1, y: 0} m_AnchorMax: {x: 1, y: 0} @@ -2043,6 +2227,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -2067,12 +2252,13 @@ MonoBehaviour: m_PressedTrigger: Pressed m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled - m_Interactable: 1 + m_Interactable: 0 m_TargetGraphic: {fileID: 1286463575} m_OnClick: m_PersistentCalls: m_Calls: - m_Target: {fileID: 719572998} + m_TargetAssemblyTypeName: m_MethodName: SendMessage m_Mode: 1 m_Arguments: @@ -2098,6 +2284,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2156,7 +2343,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} m_AnchoredPosition: {x: 1, y: 0} - m_SizeDelta: {x: -1, y: -595} + m_SizeDelta: {x: -1, y: 0} m_Pivot: {x: 0, y: 1} --- !u!114 &1335915326 MonoBehaviour: @@ -2197,6 +2384,7 @@ MonoBehaviour: m_ChildControlHeight: 1 m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!1 &1453327784 GameObject: m_ObjectHideFlags: 0 @@ -2256,6 +2444,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!223 &1453327787 Canvas: m_ObjectHideFlags: 0 @@ -2365,6 +2554,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2404,7 +2594,7 @@ GameObject: - component: {fileID: 1499096251} - component: {fileID: 1499096250} m_Layer: 5 - m_Name: Server + m_Name: LoginPanel m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -2467,6 +2657,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0.92941177} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2539,8 +2730,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_Color: {r: 0, g: 0.5019608, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2600,6 +2792,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2678,7 +2871,7 @@ RectTransform: m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 20, y: 20} m_Pivot: {x: 0.5, y: 0.5} @@ -2697,6 +2890,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -2795,6 +2989,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &1667679451 @@ -2811,6 +3006,139 @@ Transform: m_Father: {fileID: 0} m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1731300361 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1731300362} + - component: {fileID: 1731300365} + - component: {fileID: 1731300364} + - component: {fileID: 1731300363} + m_Layer: 5 + m_Name: ExitButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1731300362 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1731300361} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 637644699} + m_Father: {fileID: 75860996} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -20, y: -20} + m_SizeDelta: {x: 25, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1731300363 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1731300361} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1731300364} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 719572998} + m_TargetAssemblyTypeName: Mirror.Examples.Chat.ChatUI, Mirror.Examples + m_MethodName: ExitButtonOnClick + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 +--- !u!114 &1731300364 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1731300361} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1731300365 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1731300361} + m_CullTransparentMesh: 1 --- !u!1 &1783103022 GameObject: m_ObjectHideFlags: 0 @@ -2853,6 +3181,7 @@ MonoBehaviour: ReceiveWindowSize: 4096 MaxRetransmit: 40 NonAlloc: 1 + MaximizeSendReceiveBuffersToOSLimit: 1 ReliableMaxMessageSize: 298449 UnreliableMaxMessageSize: 1199 debugLog: 0 @@ -2889,7 +3218,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d0cd72391a563461f88eb3ddf120efef, type: 3} m_Name: m_EditorClassIdentifier: - dontDestroyOnLoad: 1 + dontDestroyOnLoad: 0 runInBackground: 1 autoStartServerBuild: 1 serverTickRate: 30 @@ -2899,8 +3228,7 @@ MonoBehaviour: networkAddress: localhost maxConnections: 100 authenticator: {fileID: 1783103024} - playerPrefab: {fileID: 5075528875289742095, guid: e5905ffa27de84009b346b49d518ba03, - type: 3} + playerPrefab: {fileID: 5075528875289742095, guid: e5905ffa27de84009b346b49d518ba03, type: 3} autoCreatePlayer: 1 playerSpawnMethod: 0 spawnPrefabs: [] @@ -2951,7 +3279,7 @@ RectTransform: - {fileID: 780870086} - {fileID: 423302020} m_Father: {fileID: 75860996} - m_RootOrder: 3 + m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -2973,6 +3301,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0.392} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -3152,6 +3481,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -3182,6 +3512,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1453327784} + m_TargetAssemblyTypeName: m_MethodName: SetActive m_Mode: 6 m_Arguments: @@ -3193,6 +3524,7 @@ MonoBehaviour: m_BoolArgument: 0 m_CallState: 2 - m_Target: {fileID: 1783103025} + m_TargetAssemblyTypeName: m_MethodName: StartHost m_Mode: 1 m_Arguments: @@ -3218,6 +3550,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -3290,8 +3623,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_Color: {r: 0, g: 0, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -3436,6 +3770,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: diff --git a/Assets/Mirror/Examples/Chat/Scripts/ChatAuthenticator.cs b/Assets/Mirror/Examples/Chat/Scripts/ChatAuthenticator.cs index bb37d7f..c1265dd 100644 --- a/Assets/Mirror/Examples/Chat/Scripts/ChatAuthenticator.cs +++ b/Assets/Mirror/Examples/Chat/Scripts/ChatAuthenticator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -58,7 +57,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -167,7 +166,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -176,7 +175,7 @@ public override void OnClientAuthenticate() authUsername = playerName, }; - NetworkClient.connection.Send(authRequestMessage); + NetworkClient.Send(authRequestMessage); } /// @@ -187,14 +186,14 @@ public void OnAuthResponseMessage(AuthResponseMessage msg) { if (msg.code == 100) { - Debug.Log($"Authentication Response: {msg.message}"); + Debug.Log($"Authentication Response: {msg.code} {msg.message}"); // Authentication has been accepted ClientAccept(); } else { - Debug.LogError($"Authentication Response: {msg.message}"); + Debug.LogError($"Authentication Response: {msg.code} {msg.message}"); // Authentication has been rejected // StopHost works for both host client and remote clients diff --git a/Assets/Mirror/Examples/Chat/Scripts/ChatNetworkManager.cs b/Assets/Mirror/Examples/Chat/Scripts/ChatNetworkManager.cs index d593003..947a5f1 100644 --- a/Assets/Mirror/Examples/Chat/Scripts/ChatNetworkManager.cs +++ b/Assets/Mirror/Examples/Chat/Scripts/ChatNetworkManager.cs @@ -1,15 +1,22 @@ using UnityEngine; -/* - Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager - API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html -*/ - namespace Mirror.Examples.Chat { [AddComponentMenu("")] public class ChatNetworkManager : NetworkManager { + public static new ChatNetworkManager singleton { get; private set; } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + singleton = this; + } + // Called by UI element NetworkAddressInput.OnValueChanged public void SetHostname(string hostname) { @@ -22,6 +29,9 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) if (conn.authenticationData != null) Player.playerNames.Remove((string)conn.authenticationData); + // remove connection from Dictionary of conn > names + ChatUI.connNames.Remove(conn); + base.OnServerDisconnect(conn); } diff --git a/Assets/Mirror/Examples/Chat/Scripts/ChatUI.cs b/Assets/Mirror/Examples/Chat/Scripts/ChatUI.cs index 0eaddf6..e8c8d0f 100644 --- a/Assets/Mirror/Examples/Chat/Scripts/ChatUI.cs +++ b/Assets/Mirror/Examples/Chat/Scripts/ChatUI.cs @@ -8,24 +8,29 @@ namespace Mirror.Examples.Chat public class ChatUI : NetworkBehaviour { [Header("UI Elements")] - public InputField chatMessage; - public Text chatHistory; - public Scrollbar scrollbar; + [SerializeField] Text chatHistory; + [SerializeField] Scrollbar scrollbar; + [SerializeField] InputField chatMessage; + [SerializeField] Button sendButton; - [Header("Diagnostic - Do Not Edit")] - public string localPlayerName; + // This is only set on client to the name of the local player + internal static string localPlayerName; - Dictionary connNames = new Dictionary(); + // Server-only cross-reference of connections to player names + internal static readonly Dictionary connNames = new Dictionary(); - public static ChatUI instance; + public override void OnStartServer() + { + connNames.Clear(); + } - void Awake() + public override void OnStartClient() { - instance = this; + chatHistory.text = ""; } [Command(requiresAuthority = false)] - public void CmdSend(string message, NetworkConnectionToClient sender = null) + void CmdSend(string message, NetworkConnectionToClient sender = null) { if (!connNames.ContainsKey(sender)) connNames.Add(sender, sender.identity.GetComponent().playerName); @@ -35,7 +40,7 @@ public void CmdSend(string message, NetworkConnectionToClient sender = null) } [ClientRpc] - public void RpcReceive(string playerName, string message) + void RpcReceive(string playerName, string message) { string prettyMessage = playerName == localPlayerName ? $"{playerName}: {message}" : @@ -43,6 +48,37 @@ public void RpcReceive(string playerName, string message) AppendMessage(prettyMessage); } + void AppendMessage(string message) + { + StartCoroutine(AppendAndScroll(message)); + } + + IEnumerator AppendAndScroll(string message) + { + chatHistory.text += message + "\n"; + + // it takes 2 frames for the UI to update ?!?! + yield return null; + yield return null; + + // slam the scrollbar down + scrollbar.value = 0; + } + + // Called by UI element ExitButton.OnClick + public void ExitButtonOnClick() + { + // StopHost calls both StopClient and StopServer + // StopServer does nothing on remote clients + NetworkManager.singleton.StopHost(); + } + + // Called by UI element MessageField.OnValueChanged + public void ToggleButton(string input) + { + sendButton.interactable = !string.IsNullOrWhiteSpace(input); + } + // Called by UI element MessageField.OnEndEdit public void OnEndEdit(string input) { @@ -60,22 +96,5 @@ public void SendMessage() chatMessage.ActivateInputField(); } } - - internal void AppendMessage(string message) - { - StartCoroutine(AppendAndScroll(message)); - } - - IEnumerator AppendAndScroll(string message) - { - chatHistory.text += message + "\n"; - - // it takes 2 frames for the UI to update ?!?! - yield return null; - yield return null; - - // slam the scrollbar down - scrollbar.value = 0; - } } } diff --git a/Assets/Mirror/Examples/Chat/Scripts/LoginUI.cs b/Assets/Mirror/Examples/Chat/Scripts/LoginUI.cs index a52f90a..a42f984 100644 --- a/Assets/Mirror/Examples/Chat/Scripts/LoginUI.cs +++ b/Assets/Mirror/Examples/Chat/Scripts/LoginUI.cs @@ -6,10 +6,10 @@ namespace Mirror.Examples.Chat public class LoginUI : MonoBehaviour { [Header("UI Elements")] - public InputField usernameInput; - public Button hostButton; - public Button clientButton; - public Text errorText; + [SerializeField] internal InputField usernameInput; + [SerializeField] internal Button hostButton; + [SerializeField] internal Button clientButton; + [SerializeField] internal Text errorText; public static LoginUI instance; diff --git a/Assets/Mirror/Examples/Chat/Scripts/Player.cs b/Assets/Mirror/Examples/Chat/Scripts/Player.cs index b26fe0f..253c004 100644 --- a/Assets/Mirror/Examples/Chat/Scripts/Player.cs +++ b/Assets/Mirror/Examples/Chat/Scripts/Player.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; +using UnityEngine; namespace Mirror.Examples.Chat { public class Player : NetworkBehaviour { - public static readonly HashSet playerNames = new HashSet(); + internal static readonly HashSet playerNames = new HashSet(); - [SyncVar(hook = nameof(OnPlayerNameChanged))] - public string playerName; + [SerializeField, SyncVar] + internal string playerName; // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload [UnityEngine.RuntimeInitializeOnLoadMethod] @@ -16,14 +17,14 @@ static void ResetStatics() playerNames.Clear(); } - void OnPlayerNameChanged(string _, string newName) + public override void OnStartServer() { - ChatUI.instance.localPlayerName = playerName; + playerName = (string)connectionToClient.authenticationData; } - public override void OnStartServer() + public override void OnStartLocalPlayer() { - playerName = (string)connectionToClient.authenticationData; + ChatUI.localPlayerName = playerName; } } } diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta b/Assets/Mirror/Examples/Common.meta similarity index 77% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta rename to Assets/Mirror/Examples/Common.meta index 5c72cf0..ae8aef4 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta +++ b/Assets/Mirror/Examples/Common.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e9de45e025f26411bbb52d1aefc8d5a5 +guid: fed3df1a3855f414b913671efe3df42a folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Examples/Common/FPS.cs b/Assets/Mirror/Examples/Common/FPS.cs new file mode 100644 index 0000000..ac44bac --- /dev/null +++ b/Assets/Mirror/Examples/Common/FPS.cs @@ -0,0 +1,37 @@ +using UnityEngine; + +namespace Mirror.Examples.Common +{ + public class FPS : MonoBehaviour + { + // fps accessible to the outside + public int framesPerSecond { get; private set; } + + // configuration + public bool showGUI = true; + public bool showLog = false; + + // helpers + int count; + double startTime; + + protected void Update() + { + ++count; + if (Time.time >= startTime + 1) + { + framesPerSecond = count; + startTime = Time.time; + count = 0; + if (showLog) Debug.Log($"FPS: {framesPerSecond}"); + } + } + + protected void OnGUI() + { + if (!showGUI) return; + + GUI.Label(new Rect(Screen.width - 70, 0, 70, 25), $"FPS: {framesPerSecond}"); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Examples/Common/FPS.cs.meta b/Assets/Mirror/Examples/Common/FPS.cs.meta new file mode 100644 index 0000000..b7d2ac4 --- /dev/null +++ b/Assets/Mirror/Examples/Common/FPS.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6635375fbc6be456ea640b75add6378e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/Discovery/Prefabs/Player.prefab b/Assets/Mirror/Examples/Discovery/Prefabs/Player.prefab index 2d917ed..a380a2b 100644 --- a/Assets/Mirror/Examples/Discovery/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/Discovery/Prefabs/Player.prefab @@ -11,8 +11,8 @@ GameObject: - component: {fileID: 8463701767414927392} - component: {fileID: 435337138507318507} - component: {fileID: 8589595951595565844} - - component: {fileID: 8188542106662419882} - component: {fileID: 1410032569926419539} + - component: {fileID: 8188542106662419882} m_Layer: 0 m_Name: Player m_TagString: Untagged @@ -30,6 +30,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -53,9 +54,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -67,6 +71,7 @@ MeshRenderer: m_ProbeAnchor: {fileID: 0} m_LightProbeVolumeOverride: {fileID: 0} m_ScaleInLightmap: 1 + m_ReceiveGI: 1 m_PreserveUVs: 0 m_IgnoreNormalsForChartDetection: 0 m_ImportantGI: 0 @@ -79,6 +84,24 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &1410032569926419539 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9081919128954505657} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 2863436854 + serverOnly: 0 + visible: 0 + hasSpawned: 0 --- !u!136 &8188542106662419882 CapsuleCollider: m_ObjectHideFlags: 0 @@ -93,18 +116,3 @@ CapsuleCollider: m_Height: 2 m_Direction: 1 m_Center: {x: 0, y: 0, z: 0} ---- !u!114 &1410032569926419539 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 9081919128954505657} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} - m_Name: - m_EditorClassIdentifier: - serverOnly: 0 - m_AssetId: - m_SceneId: 0 diff --git a/Assets/Mirror/Examples/Mirror.Examples.asmdef b/Assets/Mirror/Examples/Mirror.Examples.asmdef index 67d9ce5..108d871 100644 --- a/Assets/Mirror/Examples/Mirror.Examples.asmdef +++ b/Assets/Mirror/Examples/Mirror.Examples.asmdef @@ -1,9 +1,10 @@ { "name": "Mirror.Examples", + "rootNamespace": "", "references": [ - "Mirror", - "Mirror.Components", - "Unity.TextMeshPro" + "GUID:30817c1a0e6d646d99c048fc403f5979", + "GUID:72872094b21c16e48b631b2224833d49", + "GUID:6055be8ebefd69e48b49212b09b47b2f" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Icosphere.prefab b/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Icosphere.prefab index 16b861d..238287b 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Icosphere.prefab +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Icosphere.prefab @@ -29,6 +29,7 @@ Transform: m_LocalRotation: {x: 0, y: -0, z: -0, w: 1} m_LocalPosition: {x: -0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5513112217680897778} m_RootOrder: 0 @@ -52,10 +53,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -80,6 +83,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!64 &456454062324168415 MeshCollider: m_ObjectHideFlags: 0 @@ -125,6 +129,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: -0, y: 0, z: 0} m_LocalScale: {x: 0.8, y: 0.8, z: 0.8} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5513112217680897776} m_Father: {fileID: 0} @@ -143,9 +148,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 4214084439 serverOnly: 0 visible: 0 - m_AssetId: hasSpawned: 0 --- !u!114 &-5073764247860119520 MonoBehaviour: @@ -159,17 +164,22 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0 + target: {fileID: 5513112217680897778} clientAuthority: 0 - localPositionSensitivity: 0.01 - localRotationSensitivity: 0.01 - localScaleSensitivity: 0.01 - compressRotation: 0 - interpolateScale: 0 - interpolateRotation: 1 - interpolatePosition: 1 + syncPosition: 1 + syncRotation: 1 syncScale: 0 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + onlySyncOnChange: 1 + bufferResetMultiplier: 5 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 --- !u!114 &8774992865005872063 MonoBehaviour: m_ObjectHideFlags: 0 @@ -182,6 +192,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 218520098fbe58b4b8f0963ef41953f7, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: @@ -215,6 +226,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: c709489168fec9348b7f8290ee2e8466, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 force: 12 diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Player.prefab b/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Player.prefab index a4ee312..e6ac932 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Player.prefab +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Player.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5} m_LocalScale: {x: 0.5, y: 0.1, z: 0.2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 3138541494209382947} m_RootOrder: 0 @@ -51,10 +52,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -79,6 +82,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &1480027675339556 GameObject: m_ObjectHideFlags: 0 @@ -92,9 +96,9 @@ GameObject: - component: {fileID: 114265392388239132} - component: {fileID: 8715117357206038899} - component: {fileID: 114892629901890886} - - component: {fileID: 115187108610643062} - component: {fileID: 143011667059871024} - component: {fileID: 4839740653866577337} + - component: {fileID: 115187108610643062} - component: {fileID: 1849877933717427647} - component: {fileID: 6261579163786439309} m_Layer: 0 @@ -114,6 +118,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1.08, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3138541494209382947} m_Father: {fileID: 0} @@ -132,9 +137,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 1707196853 serverOnly: 0 visible: 0 - m_AssetId: 1f4d376d8ca693049abd1744e4c79fad hasSpawned: 0 --- !u!114 &114265392388239132 MonoBehaviour: @@ -148,28 +153,22 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 1 syncMode: 0 - syncInterval: 0.1 - clientAuthority: 1 - sendInterval: 0.05 + syncInterval: 0 + target: {fileID: 4822224316094678} + clientAuthority: 0 syncPosition: 1 syncRotation: 1 syncScale: 0 - interpolatePosition: 1 - interpolateRotation: 1 - interpolateScale: 0 - bufferTimeMultiplier: 1 - bufferSizeLimit: 64 - catchupThreshold: 4 - catchupMultiplier: 0.1 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChange: 1 bufferResetMultiplier: 5 positionSensitivity: 0.01 rotationSensitivity: 0.01 scaleSensitivity: 0.01 - showGizmos: 0 - showOverlay: 0 - overlayColor: {r: 0, g: 0, b: 0, a: 0.5} --- !u!114 &8715117357206038899 MonoBehaviour: m_ObjectHideFlags: 0 @@ -182,6 +181,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 6cac48dc20f866148b44d0f9b5850761, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 --- !u!114 &114892629901890886 @@ -191,43 +191,31 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1480027675339556} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 479a5196564ede84791870b414a13645, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 characterController: {fileID: 143011667059871024} - moveSpeed: 8 - turnSensitivity: 5 + controlsCanvas: {fileID: 0} + moveSpeedMultiplier: 8 maxTurnSpeed: 100 + turnDelta: 1 + initialJumpSpeed: 0.2 + maxJumpSpeed: 5 + jumpDelta: 0.2 + groundState: 2 horizontal: 0 vertical: 0 - turn: 0 + turnSpeed: 0 jumpSpeed: 0 - isGrounded: 1 - isFalling: 0 + animVelocity: 0 + animRotation: 0 velocity: {x: 0, y: 0, z: 0} ---- !u!114 &115187108610643062 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1480027675339556} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 8be750efa9df50f47b65ae156053d149, type: 3} - m_Name: - m_EditorClassIdentifier: - syncMode: 0 - syncInterval: 0 - playerNumber: 0 - scoreIndex: 0 - matchIndex: 0 - score: 0 - clientMatchIndex: -1 + direction: {x: 0, y: 0, z: 0} --- !u!143 &143011667059871024 CharacterController: m_ObjectHideFlags: 0 @@ -243,8 +231,8 @@ CharacterController: m_Radius: 0.5 m_SlopeLimit: 45 m_StepOffset: 0.3 - m_SkinWidth: 0.001 - m_MinMoveDistance: 0.001 + m_SkinWidth: 0.02 + m_MinMoveDistance: 0 m_Center: {x: 0, y: 0, z: 0} --- !u!136 &4839740653866577337 CapsuleCollider: @@ -260,6 +248,26 @@ CapsuleCollider: m_Height: 2 m_Direction: 1 m_Center: {x: 0, y: 0, z: 0} +--- !u!114 &115187108610643062 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1480027675339556} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8be750efa9df50f47b65ae156053d149, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + playerNumber: 0 + scoreIndex: 0 + matchIndex: 0 + score: 0 + clientMatchIndex: -1 --- !u!54 &1849877933717427647 Rigidbody: m_ObjectHideFlags: 0 @@ -288,6 +296,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 218520098fbe58b4b8f0963ef41953f7, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: @@ -321,6 +330,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 4216737524944602} m_Father: {fileID: 4822224316094678} @@ -345,10 +355,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -373,3 +385,4 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Prize.prefab b/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Prize.prefab index c08171f..10884a4 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Prize.prefab +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Prefabs/Prize.prefab @@ -31,6 +31,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7524893234998283593} m_Father: {fileID: 0} @@ -78,9 +79,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 2803719529 serverOnly: 0 visible: 0 - m_AssetId: hasSpawned: 0 --- !u!114 &114048121767222990 MonoBehaviour: @@ -94,6 +95,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 10da7fdf8caa1eb4697658bf129457fa, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 available: 1 @@ -110,6 +112,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 218520098fbe58b4b8f0963ef41953f7, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 color: @@ -143,6 +146,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0.3, y: 0.3, z: 0.3} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 4362442735993418} m_RootOrder: 0 @@ -166,10 +170,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -194,3 +200,4 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scenes/Main.unity b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scenes/Main.unity index b01ced7..b401e19 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scenes/Main.unity +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scenes/Main.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 0 m_GISettings: serializedVersion: 2 @@ -98,7 +98,7 @@ LightmapSettings: m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_UseShadowmask: 1 + m_LightingSettings: {fileID: 2039419912} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -118,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -200,6 +202,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -236,7 +239,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} m_Name: m_EditorClassIdentifier: - showGUI: 1 offsetX: 0 offsetY: 0 --- !u!114 &69965668 @@ -264,26 +266,23 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: dontDestroyOnLoad: 0 - PersistNetworkManagerToOfflineScene: 0 runInBackground: 1 autoStartServerBuild: 1 - serverTickRate: 30 + autoConnectClientBuild: 0 + sendRate: 30 offlineScene: onlineScene: transport: {fileID: 69965671} networkAddress: localhost maxConnections: 100 - disconnectInactiveConnections: 0 - disconnectInactiveTimeout: 60 authenticator: {fileID: 0} - playerPrefab: {fileID: 1480027675339556, guid: 1f4d376d8ca693049abd1744e4c79fad, - type: 3} + playerPrefab: {fileID: 1480027675339556, guid: 1f4d376d8ca693049abd1744e4c79fad, type: 3} autoCreatePlayer: 1 playerSpawnMethod: 1 spawnPrefabs: - {fileID: 1139254171913846, guid: 8cec47ed46e0eff45966a5173d3aa0d3, type: 3} - rewardPrefab: {fileID: 1139254171913846, guid: 8cec47ed46e0eff45966a5173d3aa0d3, - type: 3} + timeInterpolationGui: 0 + rewardPrefab: {fileID: 1139254171913846, guid: 8cec47ed46e0eff45966a5173d3aa0d3, type: 3} instances: 2 gameScene: Assets/Mirror/Examples/MultipleAdditiveScenes/Scenes/Game.unity --- !u!4 &69965670 @@ -296,6 +295,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 2 @@ -318,10 +318,12 @@ MonoBehaviour: Interval: 10 Timeout: 10000 FastResend: 2 - CongestionWindow: 0 SendWindowSize: 4096 ReceiveWindowSize: 4096 - NonAlloc: 1 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 298449 + UnreliableMaxMessageSize: 1199 debugLog: 0 statisticsGUI: 0 statisticsLog: 0 @@ -352,6 +354,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: -15} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 4 @@ -395,6 +398,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} m_LocalPosition: {x: -15, y: 0, z: -15} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 5 @@ -438,6 +442,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} m_LocalPosition: {x: -15, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 6 @@ -530,6 +535,7 @@ Light: m_UseColorTemperature: 0 m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 m_ShadowRadius: 0 m_ShadowAngle: 0 --- !u!4 &690741349 @@ -542,6 +548,7 @@ Transform: m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} m_LocalPosition: {x: 0, y: 10, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 1 @@ -573,6 +580,7 @@ Transform: m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} m_LocalPosition: {x: 0, y: 0, z: 15} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 0 @@ -616,6 +624,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.92387956, z: 0, w: 0.38268343} m_LocalPosition: {x: -15, y: 0, z: 15} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 7 @@ -632,6 +641,74 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!1 &1067574963 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1067574966} + - component: {fileID: 1067574965} + - component: {fileID: 1067574964} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1067574964 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1067574963} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &1067574965 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1067574963} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1067574966 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1067574963} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1445635739 GameObject: m_ObjectHideFlags: 0 @@ -658,6 +735,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 2, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 733367780} - {fileID: 2127619492} @@ -697,6 +775,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.3826836, z: -0, w: -0.92387944} m_LocalPosition: {x: 15, y: 0, z: -15} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 3 @@ -740,6 +819,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.7071068, z: -0, w: -0.7071068} m_LocalPosition: {x: 15, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 2 @@ -756,6 +836,68 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!850595691 &2039419912 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 4 + m_GIWorkflowMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_LightmapCompression: 3 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_PVRTiledBaking: 0 --- !u!1 &2127619491 GameObject: m_ObjectHideFlags: 0 @@ -783,6 +925,7 @@ Transform: m_LocalRotation: {x: 0, y: 0.9238796, z: -0, w: -0.38268325} m_LocalPosition: {x: 15, y: 0, z: 15} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1445635740} m_RootOrder: 1 diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/MultiSceneNetManager.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/MultiSceneNetManager.cs index 116b59b..a63e905 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/MultiSceneNetManager.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/MultiSceneNetManager.cs @@ -3,11 +3,6 @@ using UnityEngine; using UnityEngine.SceneManagement; -/* - Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager - API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html -*/ - namespace Mirror.Examples.MultipleAdditiveScenes { [AddComponentMenu("")] @@ -32,6 +27,18 @@ public class MultiSceneNetManager : NetworkManager // Sequential index used in round-robin deployment of players into instances and score positioning int clientIndex; + public static new MultiSceneNetManager singleton { get; private set; } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + singleton = this; + } + #region Server System Callbacks /// @@ -118,7 +125,8 @@ public override void OnStopServer() IEnumerator ServerUnloadSubScenes() { for (int index = 0; index < subScenes.Count; index++) - yield return SceneManager.UnloadSceneAsync(subScenes[index]); + if (subScenes[index].IsValid()) + yield return SceneManager.UnloadSceneAsync(subScenes[index]); subScenes.Clear(); subscenesLoaded = false; @@ -131,8 +139,8 @@ IEnumerator ServerUnloadSubScenes() /// public override void OnStopClient() { - // make sure we're not in host mode - if (mode == NetworkManagerMode.ClientOnly) + // Make sure we're not in ServerOnly mode now after stopping host client + if (mode == NetworkManagerMode.Offline) StartCoroutine(ClientUnloadSubScenes()); } @@ -140,10 +148,8 @@ public override void OnStopClient() IEnumerator ClientUnloadSubScenes() { for (int index = 0; index < SceneManager.sceneCount; index++) - { if (SceneManager.GetSceneAt(index) != SceneManager.GetActiveScene()) yield return SceneManager.UnloadSceneAsync(SceneManager.GetSceneAt(index)); - } } #endregion diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PhysicsSimulator.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PhysicsSimulator.cs index a7a7394..86008a7 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PhysicsSimulator.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PhysicsSimulator.cs @@ -26,10 +26,9 @@ void Awake() } } + [ServerCallback] void FixedUpdate() { - if (!NetworkServer.active) return; - if (simulatePhysicsScene) physicsScene.Simulate(Time.fixedDeltaTime); diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerCamera.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerCamera.cs index b61c18a..232a9df 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerCamera.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerCamera.cs @@ -24,6 +24,8 @@ public override void OnStartLocalPlayer() mainCam.transform.localPosition = new Vector3(0f, 3f, -8f); mainCam.transform.localEulerAngles = new Vector3(10f, 0f, 0f); } + else + Debug.LogWarning("PlayerCamera: Could not find a camera in scene with 'MainCamera' tag."); } public override void OnStopLocalPlayer() @@ -33,6 +35,7 @@ public override void OnStopLocalPlayer() mainCam.transform.SetParent(null); SceneManager.MoveGameObjectToScene(mainCam.gameObject, SceneManager.GetActiveScene()); mainCam.orthographic = true; + mainCam.orthographicSize = 15f; mainCam.transform.localPosition = new Vector3(0f, 70f, 0f); mainCam.transform.localEulerAngles = new Vector3(90f, 0f, 0f); } diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerController.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerController.cs index b8a1c00..5d221bc 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerController.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/PlayerController.cs @@ -1,5 +1,4 @@ using UnityEngine; -using UnityEngine.SceneManagement; namespace Mirror.Examples.MultipleAdditiveScenes { @@ -9,88 +8,177 @@ namespace Mirror.Examples.MultipleAdditiveScenes [RequireComponent(typeof(Rigidbody))] public class PlayerController : NetworkBehaviour { + public enum GroundState : byte { Jumping, Falling, Grounded } + + [Header("Avatar Components")] public CharacterController characterController; + [Header("Movement")] + [Range(1, 20)] + public float moveSpeedMultiplier = 8f; + + [Header("Turning")] + [Range(1f, 200f)] + public float maxTurnSpeed = 100f; + [Range(.5f, 5f)] + public float turnDelta = 3f; + + [Header("Jumping")] + [Range(0.1f, 1f)] + public float initialJumpSpeed = 0.2f; + [Range(1f, 10f)] + public float maxJumpSpeed = 5f; + [Range(0.1f, 1f)] + public float jumpDelta = 0.2f; + + [Header("Diagnostics - Do Not Modify")] + public GroundState groundState = GroundState.Grounded; + + [Range(-1f, 1f)] + public float horizontal; + [Range(-1f, 1f)] + public float vertical; + + [Range(-200f, 200f)] + public float turnSpeed; + + [Range(-10f, 10f)] + public float jumpSpeed; + + [Range(-1.5f, 1.5f)] + public float animVelocity; + + [Range(-1.5f, 1.5f)] + public float animRotation; + + public Vector3Int velocity; + public Vector3 direction; + void OnValidate() { if (characterController == null) characterController = GetComponent(); + // Override CharacterController default values characterController.enabled = false; + characterController.skinWidth = 0.02f; + characterController.minMoveDistance = 0f; + GetComponent().isKinematic = true; - GetComponent().clientAuthority = true; + + this.enabled = false; } - public override void OnStartLocalPlayer() + public override void OnStartAuthority() { characterController.enabled = true; + this.enabled = true; } - [Header("Movement Settings")] - public float moveSpeed = 8f; - public float turnSensitivity = 5f; - public float maxTurnSpeed = 100f; - - [Header("Diagnostics")] - public float horizontal; - public float vertical; - public float turn; - public float jumpSpeed; - public bool isGrounded = true; - public bool isFalling; - public Vector3 velocity; + public override void OnStopAuthority() + { + this.enabled = false; + characterController.enabled = false; + } void Update() { - if (!isLocalPlayer || characterController == null || !characterController.enabled) + if (!characterController.enabled) return; - horizontal = Input.GetAxis("Horizontal"); - vertical = Input.GetAxis("Vertical"); + HandleTurning(); + HandleJumping(); + HandleMove(); - // Q and E cancel each other out, reducing the turn to zero + // Reset ground state + if (characterController.isGrounded) + groundState = GroundState.Grounded; + else if (groundState != GroundState.Jumping) + groundState = GroundState.Falling; + + // Diagnostic velocity...FloorToInt for display purposes + velocity = Vector3Int.FloorToInt(characterController.velocity); + } + + // TODO: Turning works while airborne...feature? + void HandleTurning() + { + // Q and E cancel each other out, reducing the turn to zero. if (Input.GetKey(KeyCode.Q)) - turn = Mathf.MoveTowards(turn, -maxTurnSpeed, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, -maxTurnSpeed, turnDelta); if (Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, maxTurnSpeed, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, maxTurnSpeed, turnDelta); + + // If both pressed, reduce turning speed toward zero. if (Input.GetKey(KeyCode.Q) && Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, 0, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, 0, turnDelta); + + // If neither pressed, reduce turning speed toward zero. if (!Input.GetKey(KeyCode.Q) && !Input.GetKey(KeyCode.E)) - turn = Mathf.MoveTowards(turn, 0, turnSensitivity); + turnSpeed = Mathf.MoveTowards(turnSpeed, 0, turnDelta); - if (isGrounded) - isFalling = false; + transform.Rotate(0f, turnSpeed * Time.deltaTime, 0f); + } - if ((isGrounded || !isFalling) && jumpSpeed < 1f && Input.GetKey(KeyCode.Space)) + void HandleJumping() + { + // Handle variable force jumping. + // Jump starts with initial power on takeoff, and jumps higher / longer + // as player holds spacebar. Jump power is increased by a diminishing amout + // every frame until it reaches maxJumpSpeed, or player releases the spacebar, + // and then changes to the falling state until it gets grounded. + if (groundState != GroundState.Falling && Input.GetKey(KeyCode.Space)) { - jumpSpeed = Mathf.Lerp(jumpSpeed, 1f, 0.5f); + if (groundState != GroundState.Jumping) + { + // Start jump at initial power. + groundState = GroundState.Jumping; + jumpSpeed = initialJumpSpeed; + } + else + // Jumping has already started...increase power toward maxJumpSpeed over time. + jumpSpeed = Mathf.MoveTowards(jumpSpeed, maxJumpSpeed, jumpDelta); + + // If power has reached maxJumpSpeed, change to falling until grounded. + // This prevents over-applying jump power while already in the air. + if (jumpSpeed == maxJumpSpeed) + groundState = GroundState.Falling; } - else if (!isGrounded) + else if (groundState != GroundState.Grounded) { - isFalling = true; - jumpSpeed = 0; + // handles running off a cliff and/or player released Spacebar. + groundState = GroundState.Falling; + jumpSpeed = Mathf.Min(jumpSpeed, maxJumpSpeed); + jumpSpeed += Physics.gravity.y * Time.deltaTime; } + else + jumpSpeed = Physics.gravity.y * Time.deltaTime; } - void FixedUpdate() + // TODO: Directional input works while airborne...feature? + void HandleMove() { - if (!isLocalPlayer || characterController == null || !characterController.enabled) - return; + // Capture inputs + horizontal = Input.GetAxis("Horizontal"); + vertical = Input.GetAxis("Vertical"); - transform.Rotate(0f, turn * Time.fixedDeltaTime, 0f); + // Create initial direction vector without jumpSpeed (y-axis). + direction = new Vector3(horizontal, 0f, vertical); - Vector3 direction = new Vector3(horizontal, jumpSpeed, vertical); + // Clamp so diagonal strafing isn't a speed advantage. direction = Vector3.ClampMagnitude(direction, 1f); + + // Transforms direction from local space to world space. direction = transform.TransformDirection(direction); - direction *= moveSpeed; - if (jumpSpeed > 0) - characterController.Move(direction * Time.fixedDeltaTime); - else - characterController.SimpleMove(direction); + // Multiply for desired ground speed. + direction *= moveSpeedMultiplier; + + // Add jumpSpeed to direction as last step. + direction.y = jumpSpeed; - isGrounded = characterController.isGrounded; - velocity = characterController.velocity; + // Finally move the character. + characterController.Move(direction * Time.deltaTime); } } } diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/RandomColor.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/RandomColor.cs index d6d2ac8..02ad1b5 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/RandomColor.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/RandomColor.cs @@ -6,7 +6,6 @@ public class RandomColor : NetworkBehaviour { public override void OnStartServer() { - base.OnStartServer(); color = Random.ColorHSV(0f, 1f, 1f, 1f, 0.5f, 1f); } diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Reward.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Reward.cs index e3d2bc0..e06a52c 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Reward.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Reward.cs @@ -23,6 +23,7 @@ void OnTriggerEnter(Collider other) // This is called from PlayerController.CmdClaimPrize which is invoked by PlayerController.OnControllerColliderHit // This only runs on the server + [ServerCallback] public void ClaimPrize(GameObject player) { if (available) @@ -36,7 +37,6 @@ public void ClaimPrize(GameObject player) // calculate the points from the color ... lighter scores higher as the average approaches 255 // UnityEngine.Color RGB values are float fractions of 255 uint points = (uint)(((color.r) + (color.g) + (color.b)) / 3); - //Debug.Log($"Scored {points} points R:{color.r} G:{color.g} B:{color.b}"); // award the points via SyncVar on the PlayerController player.GetComponent().score += points; diff --git a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Spawner.cs b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Spawner.cs index 8646b1b..ad2ecb6 100644 --- a/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Spawner.cs +++ b/Assets/Mirror/Examples/MultipleAdditiveScenes/Scripts/Spawner.cs @@ -5,18 +5,16 @@ namespace Mirror.Examples.MultipleAdditiveScenes { internal class Spawner { + [ServerCallback] internal static void InitialSpawn(Scene scene) { - if (!NetworkServer.active) return; - for (int i = 0; i < 10; i++) SpawnReward(scene); } + [ServerCallback] internal static void SpawnReward(Scene scene) { - if (!NetworkServer.active) return; - Vector3 spawnPosition = new Vector3(Random.Range(-19, 20), 1, Random.Range(-19, 20)); GameObject reward = Object.Instantiate(((MultiSceneNetManager)NetworkManager.singleton).rewardPrefab, spawnPosition, Quaternion.identity); SceneManager.MoveGameObjectToScene(reward, scene); diff --git a/Assets/Mirror/Examples/MultipleMatches/Prefabs/CellGUI.prefab b/Assets/Mirror/Examples/MultipleMatches/Prefabs/CellGUI.prefab index 6fb790b..ab42641 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Prefabs/CellGUI.prefab +++ b/Assets/Mirror/Examples/MultipleMatches/Prefabs/CellGUI.prefab @@ -30,6 +30,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -56,12 +57,14 @@ MonoBehaviour: m_GameObject: {fileID: 4903779300334215825} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] @@ -74,6 +77,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!114 &9218727033388977555 MonoBehaviour: m_ObjectHideFlags: 0 @@ -83,11 +87,12 @@ MonoBehaviour: m_GameObject: {fileID: 4903779300334215825} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -97,17 +102,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 2985632515421915780} @@ -115,6 +123,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 2439948753870522382} + m_TargetAssemblyTypeName: m_MethodName: MakePlay m_Mode: 1 m_Arguments: diff --git a/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchController.prefab b/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchController.prefab index cba01f5..0e5cd3a 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchController.prefab +++ b/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchController.prefab @@ -28,6 +28,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7845801290156398150} - {fileID: 6007826401523109089} @@ -111,6 +112,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 6235856198463648228} - {fileID: 9152319602512786093} @@ -169,6 +171,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!225 &4694292283357099176 CanvasGroup: m_ObjectHideFlags: 0 @@ -211,9 +214,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 1992174301 serverOnly: 0 visible: 0 - m_AssetId: e101d385946c91b4c81f318efc723a88 hasSpawned: 0 --- !u!114 &4082668978785527835 MonoBehaviour: @@ -227,6 +230,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 5d17e718851449a6879986e45c458fb7, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 --- !u!114 &6069480080749678769 @@ -241,6 +245,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9ccb1dd1fc7cc624e9bff1d0d7a5c741, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 canvasGroup: {fileID: 4694292283357099176} @@ -283,6 +288,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 6237100814274483571} m_Father: {fileID: 2234053401096987375} @@ -316,6 +322,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -344,6 +351,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -374,6 +382,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 6069480080749678769} + m_TargetAssemblyTypeName: m_MethodName: RequestExitGame m_Mode: 1 m_Arguments: @@ -413,6 +422,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5868794740851378326} m_Father: {fileID: 2234053401096987375} @@ -446,6 +456,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -474,6 +485,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -504,6 +516,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 6069480080749678769} + m_TargetAssemblyTypeName: m_MethodName: RequestPlayAgain m_Mode: 1 m_Arguments: @@ -543,6 +556,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2234053401096987375} m_RootOrder: 5 @@ -575,6 +589,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -637,6 +652,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2234053401096987375} m_RootOrder: 0 @@ -669,6 +685,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -731,6 +748,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2234053401096987375} m_RootOrder: 4 @@ -763,6 +781,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -824,6 +843,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 8089560656303244059} m_RootOrder: 0 @@ -856,6 +876,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -902,6 +923,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5427396184031506795} m_RootOrder: 0 @@ -934,6 +956,7 @@ MonoBehaviour: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: @@ -959,123 +982,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 128 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: B3 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 7 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1083,8 +1082,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &8092575970934441626 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 579039501550898351} m_PrefabAsset: {fileID: 0} --- !u!1001 &1487965888931322995 @@ -1094,123 +1092,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 1 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: A1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1218,8 +1192,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &7845801290156398150 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 1487965888931322995} m_PrefabAsset: {fileID: 0} --- !u!1001 &2902895493631791574 @@ -1229,123 +1202,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 16 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: B2 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 4 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1353,8 +1302,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &5768719360570285027 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 2902895493631791574} m_PrefabAsset: {fileID: 0} --- !u!1001 &3109636500741621460 @@ -1364,123 +1312,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 2 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: B1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1488,8 +1412,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &6007826401523109089 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 3109636500741621460} m_PrefabAsset: {fileID: 0} --- !u!1001 &4456088094002665321 @@ -1499,123 +1422,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 8 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: A2 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 3 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1623,8 +1522,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &5012654960586055004 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 4456088094002665321} m_PrefabAsset: {fileID: 0} --- !u!1001 &5328567489950307008 @@ -1634,123 +1532,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 32 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: C2 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1758,8 +1632,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &3581888124560234741 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 5328567489950307008} m_PrefabAsset: {fileID: 0} --- !u!1001 &7216004328368886009 @@ -1769,123 +1642,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 4 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: C1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 2 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -1893,8 +1742,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &2045714052640889548 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 7216004328368886009} m_PrefabAsset: {fileID: 0} --- !u!1001 &7534684980187888381 @@ -1904,123 +1752,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 64 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: A3 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 6 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -2028,8 +1852,7 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &1213482271452262600 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 7534684980187888381} m_PrefabAsset: {fileID: 0} --- !u!1001 &8762608952144385888 @@ -2039,123 +1862,99 @@ PrefabInstance: m_Modification: m_TransformParent: {fileID: 9152319602512786093} m_Modifications: - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: cellValue value: 256 objectReference: {fileID: 0} - - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 2439948753870522382, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: matchController value: objectReference: {fileID: 6069480080749678769} - - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 4903779300334215825, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Name value: C3 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.x value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_Pivot.y value: 0.5 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_RootOrder value: 8 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchorMin.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_SizeDelta.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalPosition.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.w value: 1 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + - target: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} @@ -2163,7 +1962,6 @@ PrefabInstance: m_SourcePrefab: {fileID: 100100000, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} --- !u!224 &134186364329385301 stripped RectTransform: - m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, - type: 3} + m_CorrespondingSourceObject: {fileID: 8667093524748208693, guid: 5ab8c221183f0094eb04b7f6eb569e7b, type: 3} m_PrefabInstance: {fileID: 8762608952144385888} m_PrefabAsset: {fileID: 0} diff --git a/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchGUI.prefab b/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchGUI.prefab index 5a929fd..bb96954 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchGUI.prefab +++ b/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchGUI.prefab @@ -28,6 +28,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 3533216216399874122} m_RootOrder: 1 @@ -54,12 +55,14 @@ MonoBehaviour: m_GameObject: {fileID: 732536732566260629} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0.78431374} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] @@ -107,6 +110,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 8536989563668766087} - {fileID: 6013388882030083517} @@ -135,12 +139,14 @@ MonoBehaviour: m_GameObject: {fileID: 3533216216399874123} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0.19607843} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] @@ -153,6 +159,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!114 &8871144159682159596 MonoBehaviour: m_ObjectHideFlags: 0 @@ -162,11 +169,12 @@ MonoBehaviour: m_GameObject: {fileID: 3533216216399874123} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 2109663825, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -176,17 +184,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 8159776287394259959} @@ -242,6 +253,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 3533216216399874122} m_RootOrder: 0 @@ -268,12 +280,14 @@ MonoBehaviour: m_GameObject: {fileID: 5341423849544309394} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0.78431374} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] diff --git a/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchPlayer.prefab b/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchPlayer.prefab index dd41ad6..a3dab28 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchPlayer.prefab +++ b/Assets/Mirror/Examples/MultipleMatches/Prefabs/MatchPlayer.prefab @@ -28,6 +28,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 @@ -45,9 +46,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 4232591090 serverOnly: 0 visible: 0 - m_AssetId: 11d2414f1e120a14c98cba6866e5348f hasSpawned: 0 --- !u!114 &-3992624706284245286 MonoBehaviour: @@ -61,5 +62,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 5d17e718851449a6879986e45c458fb7, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 0 syncMode: 0 syncInterval: 0.1 diff --git a/Assets/Mirror/Examples/MultipleMatches/Prefabs/PlayerGUI.prefab b/Assets/Mirror/Examples/MultipleMatches/Prefabs/PlayerGUI.prefab index 4c7a2ab..a3370cb 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Prefabs/PlayerGUI.prefab +++ b/Assets/Mirror/Examples/MultipleMatches/Prefabs/PlayerGUI.prefab @@ -29,6 +29,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 4975002542353798605} m_Father: {fileID: 0} @@ -56,12 +57,14 @@ MonoBehaviour: m_GameObject: {fileID: 2742256683483721700} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.2509804, g: 0.2509804, b: 0.2509804, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] @@ -74,6 +77,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!114 &8081368318735942264 MonoBehaviour: m_ObjectHideFlags: 0 @@ -116,6 +120,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 4805460816392144898} m_RootOrder: 0 @@ -142,12 +147,14 @@ MonoBehaviour: m_GameObject: {fileID: 4667088617155248724} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] @@ -174,7 +181,7 @@ MonoBehaviour: m_GameObject: {fileID: 4667088617155248724} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -900027084, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: e19747de3f5aca642ab2be37e372fb86, type: 3} m_Name: m_EditorClassIdentifier: m_EffectColor: {r: 0, g: 0, b: 0, a: 0.5} diff --git a/Assets/Mirror/Examples/MultipleMatches/Scripts/CanvasController.cs b/Assets/Mirror/Examples/MultipleMatches/Scripts/CanvasController.cs index 872892b..ff63f50 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Scripts/CanvasController.cs +++ b/Assets/Mirror/Examples/MultipleMatches/Scripts/CanvasController.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -113,10 +114,9 @@ void ResetCanvas() /// Called from /// /// + [ClientCallback] public void SelectMatch(Guid matchId) { - if (!NetworkClient.active) return; - if (matchId == Guid.Empty) { selectedMatch = Guid.Empty; @@ -139,116 +139,96 @@ public void SelectMatch(Guid matchId) /// /// Assigned in inspector to Create button /// + [ClientCallback] public void RequestCreateMatch() { - if (!NetworkClient.active) return; - - NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Create }); + NetworkClient.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Create }); } /// /// Assigned in inspector to Join button /// + [ClientCallback] public void RequestJoinMatch() { - if (!NetworkClient.active || selectedMatch == Guid.Empty) return; + if (selectedMatch == Guid.Empty) return; - NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Join, matchId = selectedMatch }); + NetworkClient.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Join, matchId = selectedMatch }); } /// /// Assigned in inspector to Leave button /// + [ClientCallback] public void RequestLeaveMatch() { - if (!NetworkClient.active || localJoinedMatch == Guid.Empty) return; + if (localJoinedMatch == Guid.Empty) return; - NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Leave, matchId = localJoinedMatch }); + NetworkClient.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Leave, matchId = localJoinedMatch }); } /// /// Assigned in inspector to Cancel button /// + [ClientCallback] public void RequestCancelMatch() { - if (!NetworkClient.active || localPlayerMatch == Guid.Empty) return; + if (localPlayerMatch == Guid.Empty) return; - NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Cancel }); + NetworkClient.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Cancel }); } /// /// Assigned in inspector to Ready button /// + [ClientCallback] public void RequestReadyChange() { - if (!NetworkClient.active || (localPlayerMatch == Guid.Empty && localJoinedMatch == Guid.Empty)) return; + if (localPlayerMatch == Guid.Empty && localJoinedMatch == Guid.Empty) return; Guid matchId = localPlayerMatch == Guid.Empty ? localJoinedMatch : localPlayerMatch; - NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Ready, matchId = matchId }); + NetworkClient.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Ready, matchId = matchId }); } /// /// Assigned in inspector to Start button /// + [ClientCallback] public void RequestStartMatch() { - if (!NetworkClient.active || localPlayerMatch == Guid.Empty) return; + if (localPlayerMatch == Guid.Empty) return; - NetworkClient.connection.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Start }); + NetworkClient.Send(new ServerMatchMessage { serverMatchOperation = ServerMatchOperation.Start }); } /// /// Called from /// + [ClientCallback] public void OnMatchEnded() { - if (!NetworkClient.active) return; - localPlayerMatch = Guid.Empty; localJoinedMatch = Guid.Empty; ShowLobbyView(); } - /// - /// Sends updated match list to all waiting connections or just one if specified - /// - /// - internal void SendMatchList(NetworkConnectionToClient conn = null) - { - if (!NetworkServer.active) return; - - if (conn != null) - { - conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.List, matchInfos = openMatches.Values.ToArray() }); - } - else - { - foreach (NetworkConnectionToClient waiter in waitingConnections) - { - waiter.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.List, matchInfos = openMatches.Values.ToArray() }); - } - } - } - #endregion #region Server & Client Callbacks // Methods in this section are called from MatchNetworkManager's corresponding methods + [ServerCallback] internal void OnStartServer() { - if (!NetworkServer.active) return; - InitializeData(); NetworkServer.RegisterHandler(OnServerMatchMessage); } + [ServerCallback] internal void OnServerReady(NetworkConnectionToClient conn) { - if (!NetworkServer.active) return; - waitingConnections.Add(conn); playerInfos.Add(conn, new PlayerInfo { playerIndex = this.playerIndex, ready = false }); playerIndex++; @@ -256,10 +236,9 @@ internal void OnServerReady(NetworkConnectionToClient conn) SendMatchList(); } - internal void OnServerDisconnect(NetworkConnectionToClient conn) + [ServerCallback] + internal IEnumerator OnServerDisconnect(NetworkConnectionToClient conn) { - if (!NetworkServer.active) return; - // Invoke OnPlayerDisconnected on all instances of MatchController OnPlayerDisconnected?.Invoke(conn); @@ -280,9 +259,7 @@ internal void OnServerDisconnect(NetworkConnectionToClient conn) } foreach (KeyValuePair> kvp in matchConnections) - { kvp.Value.Remove(conn); - } PlayerInfo playerInfo = playerInfos[conn]; if (playerInfo.matchId != Guid.Empty) @@ -300,32 +277,31 @@ internal void OnServerDisconnect(NetworkConnectionToClient conn) PlayerInfo[] infos = connections.Select(playerConn => playerInfos[playerConn]).ToArray(); foreach (NetworkConnectionToClient playerConn in matchConnections[playerInfo.matchId]) - { if (playerConn != conn) - { playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos }); - } - } } } SendMatchList(); + + yield return null; } + [ServerCallback] internal void OnStopServer() { ResetCanvas(); } + [ClientCallback] internal void OnClientConnect() { playerInfos.Add(NetworkClient.connection, new PlayerInfo { playerIndex = this.playerIndex, ready = false }); } + [ClientCallback] internal void OnStartClient() { - if (!NetworkClient.active) return; - InitializeData(); ShowLobbyView(); createButton.gameObject.SetActive(true); @@ -333,13 +309,13 @@ internal void OnStartClient() NetworkClient.RegisterHandler(OnClientMatchMessage); } + [ClientCallback] internal void OnClientDisconnect() { - if (!NetworkClient.active) return; - InitializeData(); } + [ClientCallback] internal void OnStopClient() { ResetCanvas(); @@ -349,10 +325,9 @@ internal void OnStopClient() #region Server Match Message Handlers + [ServerCallback] void OnServerMatchMessage(NetworkConnectionToClient conn, ServerMatchMessage msg) { - if (!NetworkServer.active) return; - switch (msg.serverMatchOperation) { case ServerMatchOperation.None: @@ -393,10 +368,9 @@ void OnServerMatchMessage(NetworkConnectionToClient conn, ServerMatchMessage msg } } + [ServerCallback] void OnServerPlayerReady(NetworkConnectionToClient conn, Guid matchId) { - if (!NetworkServer.active) return; - PlayerInfo playerInfo = playerInfos[conn]; playerInfo.ready = !playerInfo.ready; playerInfos[conn] = playerInfo; @@ -405,15 +379,12 @@ void OnServerPlayerReady(NetworkConnectionToClient conn, Guid matchId) PlayerInfo[] infos = connections.Select(playerConn => playerInfos[playerConn]).ToArray(); foreach (NetworkConnectionToClient playerConn in matchConnections[matchId]) - { playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos }); - } } + [ServerCallback] void OnServerLeaveMatch(NetworkConnectionToClient conn, Guid matchId) { - if (!NetworkServer.active) return; - MatchInfo matchInfo = openMatches[matchId]; matchInfo.players--; openMatches[matchId] = matchInfo; @@ -424,26 +395,23 @@ void OnServerLeaveMatch(NetworkConnectionToClient conn, Guid matchId) playerInfos[conn] = playerInfo; foreach (KeyValuePair> kvp in matchConnections) - { kvp.Value.Remove(conn); - } HashSet connections = matchConnections[matchId]; PlayerInfo[] infos = connections.Select(playerConn => playerInfos[playerConn]).ToArray(); foreach (NetworkConnectionToClient playerConn in matchConnections[matchId]) - { playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos }); - } SendMatchList(); conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Departed }); } + [ServerCallback] void OnServerCreateMatch(NetworkConnectionToClient conn) { - if (!NetworkServer.active || playerMatches.ContainsKey(conn)) return; + if (playerMatches.ContainsKey(conn)) return; Guid newMatchId = Guid.NewGuid(); matchConnections.Add(newMatchId, new HashSet()); @@ -463,9 +431,10 @@ void OnServerCreateMatch(NetworkConnectionToClient conn) SendMatchList(); } + [ServerCallback] void OnServerCancelMatch(NetworkConnectionToClient conn) { - if (!NetworkServer.active || !playerMatches.ContainsKey(conn)) return; + if (!playerMatches.ContainsKey(conn)) return; conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Cancelled }); @@ -488,9 +457,10 @@ void OnServerCancelMatch(NetworkConnectionToClient conn) } } + [ServerCallback] void OnServerStartMatch(NetworkConnectionToClient conn) { - if (!NetworkServer.active || !playerMatches.ContainsKey(conn)) return; + if (!playerMatches.ContainsKey(conn)) return; Guid matchId; if (playerMatches.TryGetValue(conn, out matchId)) @@ -510,13 +480,9 @@ void OnServerStartMatch(NetworkConnectionToClient conn) NetworkServer.AddPlayerForConnection(playerConn, player); if (matchController.player1 == null) - { matchController.player1 = playerConn.identity; - } else - { matchController.player2 = playerConn.identity; - } /* Reset ready state for after the match. */ PlayerInfo playerInfo = playerInfos[playerConn]; @@ -536,9 +502,10 @@ void OnServerStartMatch(NetworkConnectionToClient conn) } } + [ServerCallback] void OnServerJoinMatch(NetworkConnectionToClient conn, Guid matchId) { - if (!NetworkServer.active || !matchConnections.ContainsKey(matchId) || !openMatches.ContainsKey(matchId)) return; + if (!matchConnections.ContainsKey(matchId) || !openMatches.ContainsKey(matchId)) return; MatchInfo matchInfo = openMatches[matchId]; matchInfo.players++; @@ -556,19 +523,30 @@ void OnServerJoinMatch(NetworkConnectionToClient conn, Guid matchId) conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.Joined, matchId = matchId, playerInfos = infos }); foreach (NetworkConnectionToClient playerConn in matchConnections[matchId]) - { playerConn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.UpdateRoom, playerInfos = infos }); - } + } + + /// + /// Sends updated match list to all waiting connections or just one if specified + /// + /// + [ServerCallback] + internal void SendMatchList(NetworkConnectionToClient conn = null) + { + if (conn != null) + conn.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.List, matchInfos = openMatches.Values.ToArray() }); + else + foreach (NetworkConnectionToClient waiter in waitingConnections) + waiter.Send(new ClientMatchMessage { clientMatchOperation = ClientMatchOperation.List, matchInfos = openMatches.Values.ToArray() }); } #endregion #region Client Match Message Handler + [ClientCallback] void OnClientMatchMessage(ClientMatchMessage msg) { - if (!NetworkClient.active) return; - switch (msg.clientMatchOperation) { case ClientMatchOperation.None: @@ -580,9 +558,8 @@ void OnClientMatchMessage(ClientMatchMessage msg) { openMatches.Clear(); foreach (MatchInfo matchInfo in msg.matchInfos) - { openMatches.Add(matchInfo.matchId, matchInfo); - } + RefreshMatchList(); break; } @@ -628,34 +605,32 @@ void OnClientMatchMessage(ClientMatchMessage msg) } } + [ClientCallback] void ShowLobbyView() { lobbyView.SetActive(true); roomView.SetActive(false); foreach (Transform child in matchList.transform) - { if (child.gameObject.GetComponent().GetMatchId() == selectedMatch) { Toggle toggle = child.gameObject.GetComponent(); toggle.isOn = true; - //toggle.onValueChanged.Invoke(true); } - } } + [ClientCallback] void ShowRoomView() { lobbyView.SetActive(false); roomView.SetActive(true); } + [ClientCallback] void RefreshMatchList() { foreach (Transform child in matchList.transform) - { Destroy(child.gameObject); - } joinButton.interactable = false; @@ -664,15 +639,14 @@ void RefreshMatchList() GameObject newMatch = Instantiate(matchPrefab, Vector3.zero, Quaternion.identity); newMatch.transform.SetParent(matchList.transform, false); newMatch.GetComponent().SetMatchInfo(matchInfo); - newMatch.GetComponent().group = toggleGroup; + + Toggle toggle = newMatch.GetComponent(); + toggle.group = toggleGroup; if (matchInfo.matchId == selectedMatch) - { - newMatch.GetComponent().isOn = true; - } + toggle.isOn = true; } } #endregion - } } diff --git a/Assets/Mirror/Examples/MultipleMatches/Scripts/CellGUI.cs b/Assets/Mirror/Examples/MultipleMatches/Scripts/CellGUI.cs index 1df2ec3..a6f24ec 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Scripts/CellGUI.cs +++ b/Assets/Mirror/Examples/MultipleMatches/Scripts/CellGUI.cs @@ -15,18 +15,19 @@ public class CellGUI : MonoBehaviour [Header("Diagnostics - Do Not Modify")] public NetworkIdentity playerIdentity; - public void Awake() { matchController.MatchCells.Add(cellValue, this); } + [ClientCallback] public void MakePlay() { if (matchController.currentPlayer.isLocalPlayer) matchController.CmdMakePlay(cellValue); } + [ClientCallback] public void SetPlayer(NetworkIdentity playerIdentity) { if (playerIdentity != null) diff --git a/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs b/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs index 6b99ceb..f066e92 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs +++ b/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; @@ -51,7 +52,6 @@ IEnumerator AddPlayersToMatchController() matchPlayerData.Add(player2, new MatchPlayerData { playerIndex = CanvasController.playerInfos[player2.connectionToClient].playerIndex }); } - public override void OnStartClient() { matchPlayerData.Callback += UpdateWins; @@ -64,6 +64,7 @@ public override void OnStartClient() playAgainButton.gameObject.SetActive(false); } + [ClientCallback] public void UpdateGameUI(NetworkIdentity _, NetworkIdentity newPlayerTurn) { if (!newPlayerTurn) return; @@ -80,16 +81,13 @@ public void UpdateGameUI(NetworkIdentity _, NetworkIdentity newPlayerTurn) } } + [ClientCallback] public void UpdateWins(SyncDictionary.Operation op, NetworkIdentity key, MatchPlayerData matchPlayerData) { if (key.gameObject.GetComponent().isLocalPlayer) - { winCountLocal.text = $"Player {matchPlayerData.playerIndex}\n{matchPlayerData.wins}"; - } else - { winCountOpponent.text = $"Player {matchPlayerData.playerIndex}\n{matchPlayerData.wins}"; - } } [Command(requiresAuthority = false)] @@ -128,6 +126,7 @@ public void CmdMakePlay(CellValue cellValue, NetworkConnectionToClient sender = } + [ServerCallback] bool CheckWinner(CellValue currentScore) { if ((currentScore & CellValue.TopRow) == CellValue.TopRow) @@ -159,7 +158,6 @@ public void RpcUpdateCell(CellValue cellValue, NetworkIdentity player) [ClientRpc] public void RpcShowWinner(NetworkIdentity winner) { - foreach (CellGUI cellGUI in MatchCells.Values) cellGUI.GetComponent