diff --git a/.github/workflows/RunUnityTests.yml b/.github/workflows/RunUnityTests.yml index d8b9100cd..4a45491ff 100644 --- a/.github/workflows/RunUnityTests.yml +++ b/.github/workflows/RunUnityTests.yml @@ -13,9 +13,9 @@ jobs: unityVersion: - 2019.4.40f1 - 2020.3.48f1 - - 2021.3.33f1 - - 2022.3.18f1 - - 2023.2.7f1 + - 2021.3.36f1 + - 2022.3.24f1 + - 2023.2.16f1 steps: - name: Checkout repository diff --git a/.github/workflows/Semantic.yml b/.github/workflows/Semantic.yml index 4a6a0fec8..b40004f60 100644 --- a/.github/workflows/Semantic.yml +++ b/.github/workflows/Semantic.yml @@ -21,7 +21,7 @@ jobs: Remove-Item -Recurse -Force Assets\Mirror\Tests.meta - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '3.1.100' diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs index 313f61c0b..79775a7bd 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs @@ -22,17 +22,14 @@ public static void AddDefineSymbols() HashSet defines = new HashSet(currentDefines.Split(';')) { "MIRROR", - "MIRROR_70_OR_NEWER", - "MIRROR_71_OR_NEWER", - "MIRROR_73_OR_NEWER", - "MIRROR_78_OR_NEWER", "MIRROR_79_OR_NEWER", "MIRROR_81_OR_NEWER", "MIRROR_82_OR_NEWER", "MIRROR_83_OR_NEWER", "MIRROR_84_OR_NEWER", "MIRROR_85_OR_NEWER", - "MIRROR_86_OR_NEWER" + "MIRROR_86_OR_NEWER", + "MIRROR_89_OR_NEWER" }; // only touch PlayerSettings if we actually modified it, diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs index 1b19ab729..adca7cbb1 100644 --- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs +++ b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs @@ -38,8 +38,15 @@ public class NetworkLerpRigidbody : NetworkBehaviour protected override void OnValidate() { base.OnValidate(); + Reset(); + } + + public virtual void Reset() + { if (target == null) target = GetComponent(); + + syncDirection = SyncDirection.ClientToServer; } void Update() diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs index 0c2498d7b..aa204ad33 100644 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs @@ -42,8 +42,15 @@ public class NetworkRigidbody : NetworkBehaviour protected override void OnValidate() { base.OnValidate(); + Reset(); + } + + public virtual void Reset() + { if (target == null) target = GetComponent(); + + syncDirection = SyncDirection.ClientToServer; } #region Sync vars diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs index e3bdd2992..79c07712e 100644 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs @@ -40,8 +40,15 @@ public class NetworkRigidbody2D : NetworkBehaviour protected override void OnValidate() { base.OnValidate(); + Reset(); + } + + public virtual void Reset() + { if (target == null) target = GetComponent(); + + syncDirection = SyncDirection.ClientToServer; } #region Sync vars diff --git a/Assets/Mirror/Components/LagCompensation/LagCompensator.cs b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs new file mode 100644 index 000000000..c4f398c71 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs @@ -0,0 +1,196 @@ +// Add this component to a Player object with collider. +// Automatically keeps a history for lag compensation. +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public struct Capture3D : Capture + { + public double timestamp { get; set; } + public Vector3 position; + public Vector3 size; + + public Capture3D(double timestamp, Vector3 position, Vector3 size) + { + this.timestamp = timestamp; + this.position = position; + this.size = size; + } + + public void DrawGizmo() + { + Gizmos.DrawWireCube(position, size); + } + + public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) => + new Capture3D( + 0, // interpolated snapshot is applied directly. don't need timestamps. + Vector3.LerpUnclamped(from.position, to.position, (float)t), + Vector3.LerpUnclamped(from.size, to.size, (float)t) + ); + + public override string ToString() => $"(time={timestamp} pos={position} size={size})"; + } + + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")] + public class LagCompensator : NetworkBehaviour + { + [Header("Components")] + [Tooltip("The collider to keep a history of.")] + public Collider trackedCollider; // assign this in inspector + + [Header("Settings")] + public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings(); + double lastCaptureTime; + + // lag compensation history of + readonly Queue> history = new Queue>(); + + [Header("Debugging")] + public Color historyColor = Color.white; + + [ServerCallback] + protected virtual void Update() + { + // capture lag compensation snapshots every interval. + // NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet. + if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval) + { + lastCaptureTime = NetworkTime.localTime; + Capture(); + } + } + + [ServerCallback] + protected virtual void Capture() + { + // capture current state + Capture3D capture = new Capture3D( + NetworkTime.localTime, + trackedCollider.bounds.center, + trackedCollider.bounds.size + ); + + // insert into history + LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture); + } + + protected virtual void OnDrawGizmos() + { + // draw history + Gizmos.color = historyColor; + LagCompensation.DrawGizmos(history); + } + + // sampling //////////////////////////////////////////////////////////// + // sample the sub-tick (=interpolated) history of this object for a hit test. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + [ServerCallback] + public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample) + { + // never trust the client: estimate client time instead. + // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking + // the estimation is very good. the error is as low as ~6ms for the demo. + // note that passing 'rtt' is fine: EstimateTime halves it to latency. + double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime); + + // sample the history to get the nearest snapshots around 'timestamp' + if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t)) + { + // interpolate to get a decent estimation at exactly 'timestamp' + sample = Capture3D.Interpolate(resultBefore, resultAfter, t); + return true; + } + else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}"); + + sample = default; + return false; + } + + // convenience tests /////////////////////////////////////////////////// + // there are multiple different ways to check a hit against the sample: + // - raycasting + // - bounds.contains + // - increasing bounds by tolerance and checking contains + // - threshold to bounds.closestpoint + // let's offer a few solutions directly and see which users prefer. + + // bounds check: checks distance to closest point on bounds in history @ -rtt. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + // this is super simple and fast, but not 100% physically accurate since we don't raycast. + [ServerCallback] + public virtual bool BoundsCheck( + NetworkConnectionToClient viewer, + Vector3 hitPoint, + float toleranceDistance, + out float distance, + out Vector3 nearest) + { + // first, sample the history at -rtt of the viewer. + if (Sample(viewer, out Capture3D capture)) + { + // now that we know where the other player was at that time, + // we can see if the hit point was within tolerance of it. + // TODO consider rotations??? + // TODO consider original collider shape?? + Bounds bounds = new Bounds(capture.position, capture.size); + nearest = bounds.ClosestPoint(hitPoint); + distance = Vector3.Distance(nearest, hitPoint); + return distance <= toleranceDistance; + } + nearest = hitPoint; + distance = 0; + return false; + } + + // raycast check: creates a collider the sampled position and raycasts to hitPoint. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + // this is physically accurate (checks against walls etc.), with the cost + // of a runtime instantiation. + // + // originPoint: where the player fired the weapon. + // hitPoint: where the player's local raycast hit. + // tolerance: scale up the sampled collider by % in order to have a bit of a tolerance. + // 0 means no extra tolerance, 0.05 means 5% extra tolerance. + // layerMask: the layer mask to use for the raycast. + [ServerCallback] + public virtual bool RaycastCheck( + NetworkConnectionToClient viewer, + Vector3 originPoint, + Vector3 hitPoint, + float tolerancePercent, + int layerMask, + out RaycastHit hit) + { + // first, sample the history at -rtt of the viewer. + if (Sample(viewer, out Capture3D capture)) + { + // instantiate a real physics collider on demand. + // TODO rotation?? + // TODO different collier types?? + GameObject temp = new GameObject("LagCompensatorTest"); + temp.transform.position = capture.position; + BoxCollider tempCollider = temp.AddComponent(); + tempCollider.size = capture.size * (1 + tolerancePercent); + + // raycast + Vector3 direction = hitPoint - originPoint; + float maxDistance = direction.magnitude * 2; + bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask); + + // cleanup + Destroy(temp); + return result; + } + + hit = default; + return false; + } + } +} diff --git a/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta new file mode 100644 index 000000000..d4912e490 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a898831dd60c4cdfbfd9a6ea5702ed01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkAnimator.cs b/Assets/Mirror/Components/NetworkAnimator.cs index 7510ff599..657c28fa2 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs +++ b/Assets/Mirror/Components/NetworkAnimator.cs @@ -31,11 +31,12 @@ public class NetworkAnimator : NetworkBehaviour public Animator animator; /// - /// Syncs animator.speed + /// Syncs animator.speed. + /// Default to 1 because Animator.speed defaults to 1. /// [SyncVar(hook = nameof(OnAnimatorSpeedChanged))] - float animatorSpeed; - float previousSpeed; + float animatorSpeed = 1f; + float previousSpeed = 1f; // Note: not an object[] array because otherwise initialization is real annoying int[] lastIntParameters; @@ -93,6 +94,11 @@ void Initialize() void Awake() => Initialize(); void OnEnable() => Initialize(); + public virtual void Reset() + { + syncDirection = SyncDirection.ClientToServer; + } + void FixedUpdate() { if (!SendMessagesAllowed) diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs index 4611d3b51..976a12cd4 100644 --- a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs @@ -92,5 +92,20 @@ protected override void OnValidate() Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); } } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation; + } } } diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs index 82bc25b77..28fb42292 100644 --- a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs @@ -92,5 +92,20 @@ protected override void OnValidate() Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); } } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation.eulerAngles.z; + } } } diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs index c943f18e0..7e823920e 100644 --- a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs @@ -92,5 +92,20 @@ protected override void OnValidate() Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); } } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation; + } } } diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs index c770b3378..787fb9fdd 100644 --- a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs @@ -92,5 +92,20 @@ protected override void OnValidate() Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); } } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation.eulerAngles.z; + } } } diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs index e752c2020..52a622394 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -332,8 +332,7 @@ protected virtual void OnTeleport(Vector3 destination) // but server's last delta will have been reset, causing offsets. // // instead, simply clear snapshots. - serverSnapshots.Clear(); - clientSnapshots.Clear(); + ResetState(); // TODO // what if we still receive a snapshot from before the interpolation? @@ -358,8 +357,7 @@ protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) // but server's last delta will have been reset, causing offsets. // // instead, simply clear snapshots. - serverSnapshots.Clear(); - clientSnapshots.Clear(); + ResetState(); // TODO // what if we still receive a snapshot from before the interpolation? diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs index 677904d4b..ec5d3e14a 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -1,4 +1,5 @@ // NetworkTransform V2 by mischa (2021-07) +using System.Collections.Generic; using UnityEngine; namespace Mirror @@ -12,6 +13,8 @@ public class NetworkTransformUnreliable : NetworkTransformBase // Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover. [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results,.")] public float bufferResetMultiplier = 3; + [Tooltip("Detect and send only changed data, such as Position X and Z, not the full Vector3 of X Y Z. Lowers network data at cost of extra calculations.")] + public bool changedDetection = true; [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] public float positionSensitivity = 0.01f; @@ -25,6 +28,7 @@ public class NetworkTransformUnreliable : NetworkTransformBase // Used to store last sent snapshots protected TransformSnapshot lastSnapshot; protected bool cachedSnapshotComparison; + protected Changed cachedChangedComparison; protected bool hasSentUnchangedPosition; // update ////////////////////////////////////////////////////////////// @@ -61,7 +65,9 @@ protected virtual void CheckLastSendTime() // This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame, // because intervalCounter is always = 1 in the previous version. - if (sendIntervalCounter == sendIntervalMultiplier) + // Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571 + + if (sendIntervalCounter >= sendIntervalMultiplier) sendIntervalCounter = 0; // timeAsDouble not available in older Unity versions. @@ -109,36 +115,68 @@ void UpdateServerBroadcast() // send snapshot without timestamp. // receiver gets it from batch timestamp to save bandwidth. TransformSnapshot snapshot = Construct(); - cachedSnapshotComparison = CompareSnapshots(snapshot); - if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } - if (compressRotation) + if (changedDetection) { - RpcServerToClientSyncCompressRotation( + cachedChangedComparison = CompareChangedSnapshots(snapshot); + + if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + SyncData syncData = new SyncData(cachedChangedComparison, snapshot); + + RpcServerToClientSync(syncData); + + if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + UpdateLastSentSnapshot(cachedChangedComparison, snapshot); + } + } + else + { + cachedSnapshotComparison = CompareSnapshots(snapshot); + if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + if (compressRotation) + { + RpcServerToClientSyncCompressRotation( + // only sync what the user wants to sync + syncPosition && positionChanged ? snapshot.position : default(Vector3?), + syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?), + syncScale && scaleChanged ? snapshot.scale : default(Vector3?) + ); + } + else + { + RpcServerToClientSync( // only sync what the user wants to sync syncPosition && positionChanged ? snapshot.position : default(Vector3?), - syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?), + syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), syncScale && scaleChanged ? snapshot.scale : default(Vector3?) - ); - } - else - { - 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?) - ); - } + ); + } - if (cachedSnapshotComparison) - { - hasSentUnchangedPosition = true; - } - else - { - hasSentUnchangedPosition = false; - lastSnapshot = snapshot; + if (cachedSnapshotComparison) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + + // Fixes https://github.com/MirrorNetworking/Mirror/issues/3572 + // This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573 + // with the exception of Quaternion.Angle sensitivity has to be > 0.16. + // Unity issue, we are leaving it as is. + + if (positionChanged) lastSnapshot.position = snapshot.position; + if (rotationChanged) lastSnapshot.rotation = snapshot.rotation; + if (positionChanged) lastSnapshot.scale = snapshot.scale; + } } } } @@ -205,36 +243,67 @@ void UpdateClientBroadcast() // send snapshot without timestamp. // receiver gets it from batch timestamp to save bandwidth. TransformSnapshot snapshot = Construct(); - cachedSnapshotComparison = CompareSnapshots(snapshot); - if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } - if (compressRotation) + if (changedDetection) { - CmdClientToServerSyncCompressRotation( - // only sync what the user wants to sync - syncPosition && positionChanged ? snapshot.position : default(Vector3?), - syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?), - syncScale && scaleChanged ? snapshot.scale : default(Vector3?) - ); + cachedChangedComparison = CompareChangedSnapshots(snapshot); + + if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + SyncData syncData = new SyncData(cachedChangedComparison, snapshot); + + CmdClientToServerSync(syncData); + + if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + UpdateLastSentSnapshot(cachedChangedComparison, snapshot); + } } else { - 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?) - ); - } + cachedSnapshotComparison = CompareSnapshots(snapshot); + if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } - if (cachedSnapshotComparison) - { - hasSentUnchangedPosition = true; - } - else - { - hasSentUnchangedPosition = false; - lastSnapshot = snapshot; + if (compressRotation) + { + CmdClientToServerSyncCompressRotation( + // only sync what the user wants to sync + syncPosition && positionChanged ? snapshot.position : default(Vector3?), + syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?), + syncScale && scaleChanged ? snapshot.scale : default(Vector3?) + ); + } + else + { + 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?) + ); + } + + if (cachedSnapshotComparison) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + + // Fixes https://github.com/MirrorNetworking/Mirror/issues/3572 + // This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573 + // with the exception of Quaternion.Angle sensitivity has to be > 0.16. + // Unity issue, we are leaving it as is. + if (positionChanged) lastSnapshot.position = snapshot.position; + if (rotationChanged) lastSnapshot.rotation = snapshot.rotation; + if (positionChanged) lastSnapshot.scale = snapshot.scale; + } } } } @@ -406,5 +475,204 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); } + + protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot) + { + if (change == Changed.None || change == Changed.CompressRot) return; + + if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x; + if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y; + if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z; + + if (compressRotation) + { + if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation; + } + else + { + Vector3 newRotation; + newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x; + newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y; + newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z; + + lastSnapshot.rotation = Quaternion.Euler(newRotation); + } + + if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale; + } + + // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. + // Note the sensitivity comparison are different for pos, rot and scale. + protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot) + { + Changed change = Changed.None; + + if (syncPosition) + { + bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; + if (positionChanged) + { + if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX; + if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY; + if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ; + } + } + + if (syncRotation) + { + if (compressRotation) + { + bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; + if (rotationChanged) + { + // Here we set all Rot enum flags, to tell us if there was a change in rotation + // when using compression. If no change, we don't write the compressed Quat. + change |= Changed.CompressRot; + change |= Changed.Rot; + } + else + { + change |= Changed.CompressRot; + } + } + else + { + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX; + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY; + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ; + } + } + + if (syncScale) + { + if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale; + } + + return change; + } + + [Command(channel = Channels.Unreliable)] + void CmdClientToServerSync(SyncData syncData) + { + OnClientToServerSync(syncData); + //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(syncData); + } + + protected virtual void OnClientToServerSync(SyncData syncData) + { + // 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) + { + double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval; + + if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + ResetState(); + } + + UpdateSyncData(ref syncData, serverSnapshots); + + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); + } + + + [ClientRpc(channel = Channels.Unreliable)] + void RpcServerToClientSync(SyncData syncData) => + OnServerToClientSync(syncData); + + protected virtual void OnServerToClientSync(SyncData syncData) + { + // 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) + { + double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval; + + if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + ResetState(); + } + + UpdateSyncData(ref syncData, clientSnapshots); + + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); + } + + protected virtual void UpdateSyncData(ref SyncData syncData, SortedList snapshots) + { + if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot) + { + syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition(); + syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation(); + syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale(); + } + else + { + // Just going to update these without checking if syncposition or not, + // because if not syncing position, NT will not apply any position data + // to the target during Apply(). + + syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x); + syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y); + syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z); + + // If compressRot is true, we already have the Quat in syncdata. + if ((syncData.changedDataByte & Changed.CompressRot) == 0) + { + syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x); + syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ; + syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z); + + syncData.quatRotation = Quaternion.Euler(syncData.vecRotation); + } + else + { + syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation()); + } + + syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale()); + } + } + + // This is to extract position/rotation/scale data from payload. Override + // Construct and Deconstruct if you are implementing a different SyncData logic. + // Note however that snapshot interpolation still requires the basic 3 data + // position, rotation and scale, which are computed from here. + protected virtual void DeconstructSyncData(System.ArraySegment receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload)) + { + SyncData syncData = reader.Read(); + changedFlagData = (byte)syncData.changedDataByte; + position = syncData.position; + rotation = syncData.quatRotation; + scale = syncData.scale; + } + } } } diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs new file mode 100644 index 000000000..9b6d51cb3 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs @@ -0,0 +1,156 @@ +using UnityEngine; +using System; +using Mirror; + +namespace Mirror +{ + [Serializable] + public struct SyncData + { + public Changed changedDataByte; + public Vector3 position; + public Quaternion quatRotation; + public Vector3 vecRotation; + public Vector3 scale; + + public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale) + { + this.changedDataByte = _dataChangedByte; + this.position = _position; + this.quatRotation = _rotation; + this.vecRotation = quatRotation.eulerAngles; + this.scale = _scale; + } + + public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot) + { + this.changedDataByte = _dataChangedByte; + this.position = _snapshot.position; + this.quatRotation = _snapshot.rotation; + this.vecRotation = quatRotation.eulerAngles; + this.scale = _snapshot.scale; + } + + public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale) + { + this.changedDataByte = _dataChangedByte; + this.position = _position; + this.vecRotation = _vecRotation; + this.quatRotation = Quaternion.Euler(vecRotation); + this.scale = _scale; + } + } + + [Flags] + public enum Changed : byte + { + None = 0, + PosX = 1 << 0, + PosY = 1 << 1, + PosZ = 1 << 2, + CompressRot = 1 << 3, + RotX = 1 << 4, + RotY = 1 << 5, + RotZ = 1 << 6, + Scale = 1 << 7, + + Pos = PosX | PosY | PosZ, + Rot = RotX | RotY | RotZ + } + + + public static class SyncDataReaderWriter + { + public static void WriteSyncData(this NetworkWriter writer, SyncData syncData) + { + writer.WriteByte((byte)syncData.changedDataByte); + + // Write position + if ((syncData.changedDataByte & Changed.PosX) > 0) + { + writer.WriteFloat(syncData.position.x); + } + + if ((syncData.changedDataByte & Changed.PosY) > 0) + { + writer.WriteFloat(syncData.position.y); + } + + if ((syncData.changedDataByte & Changed.PosZ) > 0) + { + writer.WriteFloat(syncData.position.z); + } + + // Write rotation + if ((syncData.changedDataByte & Changed.CompressRot) > 0) + { + if((syncData.changedDataByte & Changed.Rot) > 0) + { + writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation)); + } + } + else + { + if ((syncData.changedDataByte & Changed.RotX) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.x); + } + + if ((syncData.changedDataByte & Changed.RotY) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.y); + } + + if ((syncData.changedDataByte & Changed.RotZ) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.z); + } + } + + // Write scale + if ((syncData.changedDataByte & Changed.Scale) > 0) + { + writer.WriteVector3(syncData.scale); + } + } + + public static SyncData ReadSyncData(this NetworkReader reader) + { + Changed changedData = (Changed)reader.ReadByte(); + + // If we have nothing to read here, let's say because posX is unchanged, then we can write anything + // for now, but in the NT, we will need to check changedData again, to put the right values of the axis + // back. We don't have it here. + + Vector3 position = + new Vector3( + (changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0 + ); + + Vector3 vecRotation = new Vector3(); + Quaternion quatRotation = new Quaternion(); + + if ((changedData & Changed.CompressRot) > 0) + { + quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion(); + } + else + { + vecRotation = + new Vector3( + (changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0 + ); + } + + Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3(); + + SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale); + + return _syncData; + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta new file mode 100644 index 000000000..15bb004c0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1c0832ca88e749ff96fe04cebb617ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 3f86cab6f..0fd5e6a69 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -10,40 +10,58 @@ // instead of real physics. It's not 100% correct - but it sure is fast! using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using UnityEngine; namespace Mirror { - public enum CorrectionMode - { - Set, // rigidbody.position/rotation = ... - Move, // rigidbody.MovePosition/Rotation - } + public enum PredictionMode { Smooth, Fast } // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. public class PredictedRigidbody : NetworkBehaviour { Transform tf; // this component is performance critical. cache .transform getter! - Rigidbody rb; // own rigidbody on server. this is never moved to a physics copy. + + // Prediction sometimes moves the Rigidbody to a ghost object. + // .predictedRigidbody is always kept up to date to wherever the RB is. + // other components should use this when accessing Rigidbody. + public Rigidbody predictedRigidbody; + Transform predictedRigidbodyTransform; // predictedRigidbody.transform for performance (Get/SetPositionAndRotation) + Vector3 lastPosition; - // [Tooltip("Broadcast changes if position changed by more than ... meters.")] - // public float positionSensitivity = 0.01f; + // motion smoothing happen on-demand, because it requires moving physics components to another GameObject. + // this only starts at a given velocity and ends when stopped moving. + // to avoid constant on/off/on effects, it also stays on for a minimum time. + [Header("Motion Smoothing")] + [Tooltip("Prediction supports two different modes: Smooth and Fast:\n\nSmooth: Physics are separated from the GameObject & applied in the background. Rendering smoothly follows the physics for perfectly smooth interpolation results. Much softer, can be even too soft where sharp collisions won't look as sharp (i.e. Billiard balls avoid the wall before even hitting it).\n\nFast: Physics remain on the GameObject and corrections are applied hard. Much faster since we don't need to update a separate GameObject, a bit harsher, more precise.")] + public PredictionMode mode = PredictionMode.Smooth; + [Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")] + public float motionSmoothingVelocityThreshold = 0.1f; + float motionSmoothingVelocityThresholdSqr; // ² cached in Awake + public float motionSmoothingAngularVelocityThreshold = 5.0f; // Billiards demo: 0.1 is way too small, takes forever for IsMoving()==false + float motionSmoothingAngularVelocityThresholdSqr; // ² cached in Awake + public float motionSmoothingTimeTolerance = 0.5f; + double motionSmoothingLastMovedTime; // client keeps state history for correction & reconciliation. // this needs to be a SortedList because we need to be able to insert inbetween. - // RingBuffer would be faster iteration, but can't do insertions. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! [Header("State History")] public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough readonly SortedList stateHistory = new SortedList(); public float recordInterval = 0.050f; + [Tooltip("(Optional) performance optimization where FixedUpdate.RecordState() only inserts state into history if the state actually changed.\nThis is generally a good idea.")] + public bool onlyRecordChanges = true; + [Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")] public bool compareLastFirst = true; [Header("Reconciliation")] [Tooltip("Correction threshold in meters. For example, 0.1 means that if the client is off by more than 10cm, it gets corrected.")] public double positionCorrectionThreshold = 0.10; + double positionCorrectionThresholdSqr; // ² cached in Awake [Tooltip("Correction threshold in degrees. For example, 5 means that if the client is off by more than 5 degrees, it gets corrected.")] public double rotationCorrectionThreshold = 5; @@ -51,22 +69,22 @@ public class PredictedRigidbody : NetworkBehaviour public bool oneFrameAhead = true; [Header("Smoothing")] - [Tooltip("Configure how to apply the corrected state.")] - public CorrectionMode correctionMode = CorrectionMode.Move; - [Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")] public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal. [Header("Visual Interpolation")] [Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")] public bool showGhost = true; - public float ghostDistanceThreshold = 0.1f; - public float ghostEnabledCheckInterval = 0.2f; + [Tooltip("Physics components are moved onto a ghost object beyond this threshold. Main object visually interpolates to it.")] + public float ghostVelocityThreshold = 0.1f; [Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")] public Material localGhostMaterial; public Material remoteGhostMaterial; + [Tooltip("Performance optimization: only create/destroy ghosts every n-th frame is enough.")] + public int checkGhostsEveryNthFrame = 4; + [Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")] public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least public float rotationInterpolationSpeed = 10; @@ -74,25 +92,57 @@ public class PredictedRigidbody : NetworkBehaviour [Tooltip("Teleport if we are further than 'multiplier x collider size' behind.")] public float teleportDistanceMultiplier = 10; - [Header("Debugging")] - public float lineTime = 10; + [Header("Bandwidth")] + [Tooltip("Reduce sends while velocity==0. Client's objects may slightly move due to gravity/physics, so we still want to send corrections occasionally even if an object is idle on the server the whole time.")] + public bool reduceSendsWhileIdle = true; // Rigidbody & Collider are moved out into a separate object. // this way the visual object can smoothly follow. protected GameObject physicsCopy; - Transform physicsCopyTransform; // caching to avoid GetComponent - Rigidbody physicsCopyRigidbody; // caching to avoid GetComponent - Collider physicsCopyCollider; // caching to avoid GetComponent - float smoothFollowThreshold; // caching to avoid calculation in LateUpdate + // protected Transform physicsCopyTransform; // caching to avoid GetComponent + // protected Rigidbody physicsCopyRigidbody => rb; // caching to avoid GetComponent + // protected Collider physicsCopyCollider; // caching to avoid GetComponent + float smoothFollowThreshold; // caching to avoid calculation in LateUpdate + float smoothFollowThresholdSqr; // caching to avoid calculation in LateUpdate // we also create one extra ghost for the exact known server state. protected GameObject remoteCopy; - void Awake() + // joints + Vector3 initialPosition; + Quaternion initialRotation; + // Vector3 initialScale; // don't change scale for now. causes issues with parenting. + + Color originalColor; + + protected virtual void Awake() { tf = transform; - rb = GetComponent(); - if (rb == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component."); + predictedRigidbody = GetComponent(); + if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component."); + predictedRigidbodyTransform = predictedRigidbody.transform; + + // in fast mode, we need to force enable Rigidbody.interpolation. + // otherwise there's not going to be any smoothing whatsoever. + if (mode == PredictionMode.Fast) + { + predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate; + } + + // cache some threshold to avoid calculating them in LateUpdate + float colliderSize = GetComponentInChildren().bounds.size.magnitude; + smoothFollowThreshold = colliderSize * teleportDistanceMultiplier; + smoothFollowThresholdSqr = smoothFollowThreshold * smoothFollowThreshold; + + // cache initial position/rotation/scale to be used when moving physics components (configurable joints' range of motion) + initialPosition = tf.position; + initialRotation = tf.rotation; + // initialScale = tf.localScale; + + // cache ² computations + motionSmoothingVelocityThresholdSqr = motionSmoothingVelocityThreshold * motionSmoothingVelocityThreshold; + motionSmoothingAngularVelocityThresholdSqr = motionSmoothingAngularVelocityThreshold * motionSmoothingAngularVelocityThreshold; + positionCorrectionThresholdSqr = positionCorrectionThreshold * positionCorrectionThreshold; } protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) @@ -130,10 +180,10 @@ protected virtual void CopyRenderersAsGhost(GameObject destination, Material mat // besides, Rigidbody+Collider are two components, where as renders may be many. protected virtual void CreateGhosts() { - // skip if already separated - if (physicsCopy != null) return; + // skip if host mode or already separated + if (isServer || physicsCopy != null) return; - Debug.Log($"Separating Physics for {name}"); + // Debug.Log($"Separating Physics for {name}"); // logging this allocates too much // create an empty GameObject with the same name + _Physical // it's important to copy world position/rotation/scale, not local! @@ -146,9 +196,6 @@ protected virtual void CreateGhosts() // if we copy localScale then the copy has scale=0.5, where as the // original would have a global scale of ~1.0. physicsCopy = new GameObject($"{name}_Physical"); - physicsCopy.transform.position = tf.position; // world position! - physicsCopy.transform.rotation = tf.rotation; // world rotation! - physicsCopy.transform.localScale = tf.lossyScale; // world scale! // assign the same Layer for the physics copy. // games may use a custom physics collision matrix, layer matters. @@ -157,19 +204,32 @@ protected virtual void CreateGhosts() // add the PredictedRigidbodyPhysical component PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent(); physicsGhostRigidbody.target = tf; - physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; - physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; - // move the rigidbody component & all colliders to the physics GameObject + // when moving (Configurable)Joints, their range of motion is + // relative to the initial position. if we move them after the + // GameObject rotated, the range of motion is wrong. + // the easiest solution is to move to initial position, + // then move physics components, then move back. + // => remember previous + Vector3 position = tf.position; + Quaternion rotation = tf.rotation; + // Vector3 scale = tf.localScale; // don't change scale for now. causes issues with parenting. + // => reset to initial + physicsGhostRigidbody.transform.position = tf.position = initialPosition; + physicsGhostRigidbody.transform.rotation = tf.rotation = initialRotation; + physicsGhostRigidbody.transform.localScale = tf.lossyScale;// world scale! // = initialScale; // don't change scale for now. causes issues with parenting. + // => move physics components PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy); + // => reset previous + physicsGhostRigidbody.transform.position = tf.position = position; + physicsGhostRigidbody.transform.rotation = tf.rotation = rotation; + //physicsGhostRigidbody.transform.localScale = tf.lossyScale; // world scale! //= scale; // don't change scale for now. causes issues with parenting. // show ghost by copying all renderers / materials with ghost material applied if (showGhost) { // one for the locally predicted rigidbody CopyRenderersAsGhost(physicsCopy, localGhostMaterial); - physicsGhostRigidbody.ghostDistanceThreshold = ghostDistanceThreshold; - physicsGhostRigidbody.ghostEnabledCheckInterval = ghostEnabledCheckInterval; // one for the latest remote state for comparison // it's important to copy world position/rotation/scale, not local! @@ -185,23 +245,12 @@ protected virtual void CreateGhosts() remoteCopy.transform.position = tf.position; // world position! remoteCopy.transform.rotation = tf.rotation; // world rotation! remoteCopy.transform.localScale = tf.lossyScale; // world scale! - PredictedRigidbodyRemoteGhost predictedGhost = remoteCopy.AddComponent(); - predictedGhost.target = tf; - predictedGhost.ghostDistanceThreshold = ghostDistanceThreshold; - predictedGhost.ghostEnabledCheckInterval = ghostEnabledCheckInterval; CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial); } - // cache components to avoid GetComponent calls at runtime - physicsCopyTransform = physicsCopy.transform; - physicsCopyRigidbody = physicsCopy.GetComponent(); - physicsCopyCollider = physicsCopy.GetComponentInChildren(); - if (physicsCopyRigidbody == null) throw new Exception("SeparatePhysics: couldn't find final Rigidbody."); - if (physicsCopyCollider == null) throw new Exception("SeparatePhysics: couldn't find final Collider."); - - // cache some threshold to avoid calculating them in LateUpdate - float colliderSize = physicsCopyCollider.bounds.size.magnitude; - smoothFollowThreshold = colliderSize * teleportDistanceMultiplier; + // assign our Rigidbody reference to the ghost + predictedRigidbody = physicsCopy.GetComponent(); + predictedRigidbodyTransform = predictedRigidbody.transform; } protected virtual void DestroyGhosts() @@ -211,8 +260,32 @@ protected virtual void DestroyGhosts() // otherwise next time they wouldn't have a collider anymore. if (physicsCopy != null) { + // when moving (Configurable)Joints, their range of motion is + // relative to the initial position. if we move them after the + // GameObject rotated, the range of motion is wrong. + // the easiest solution is to move to initial position, + // then move physics components, then move back. + // => remember previous + Vector3 position = tf.position; + Quaternion rotation = tf.rotation; + Vector3 scale = tf.localScale; + // => reset to initial + physicsCopy.transform.position = tf.position = initialPosition; + physicsCopy.transform.rotation = tf.rotation = initialRotation; + physicsCopy.transform.localScale = tf.lossyScale;// = initialScale; + // => move physics components PredictionUtils.MovePhysicsComponents(physicsCopy, gameObject); + // => reset previous + tf.position = position; + tf.rotation = rotation; + tf.localScale = scale; + + // when moving components back, we need to undo the joints initial-delta rotation that we added. Destroy(physicsCopy); + + // reassign our Rigidbody reference + predictedRigidbody = GetComponent(); + predictedRigidbodyTransform = predictedRigidbody.transform; } // simply destroy the remote copy @@ -223,8 +296,8 @@ protected virtual void DestroyGhosts() protected virtual void SmoothFollowPhysicsCopy() { // hard follow: - // tf.position = physicsCopyCollider.position; - // tf.rotation = physicsCopyCollider.rotation; + // predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); + // tf.SetPositionAndRotation(physicsPosition, physicsRotation); // ORIGINAL VERSION: CLEAN AND SIMPLE /* @@ -251,14 +324,18 @@ protected virtual void SmoothFollowPhysicsCopy() */ // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! - Vector3 currentPosition = tf.position; - Quaternion currentRotation = tf.rotation; - Vector3 physicsPosition = physicsCopyTransform.position; // faster than accessing physicsCopyRigidbody! - Quaternion physicsRotation = physicsCopyTransform.rotation; // faster than accessing physicsCopyRigidbody! - float deltaTime = Time.deltaTime; + tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than tf.position + tf.rotation + predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); // faster than Rigidbody .position and .rotation + float deltaTime = Time.deltaTime; - float distance = Vector3.Distance(currentPosition, physicsPosition); - if (distance > smoothFollowThreshold) + // slow and simple version: + // float distance = Vector3.Distance(currentPosition, physicsPosition); + // if (distance > smoothFollowThreshold) + // faster version + Vector3 delta = physicsPosition - currentPosition; + float sqrDistance = Vector3.SqrMagnitude(delta); + float distance = Mathf.Sqrt(sqrDistance); + if (sqrDistance > smoothFollowThresholdSqr) { tf.SetPositionAndRotation(physicsPosition, physicsRotation); // faster than .position and .rotation manually Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {smoothFollowThreshold:F2}"); @@ -271,21 +348,42 @@ protected virtual void SmoothFollowPhysicsCopy() // sooner we need to catch the fuck up // float positionStep = (distance * distance) * interpolationSpeed; float positionStep = distance * positionInterpolationSpeed; - Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * deltaTime); + + Vector3 newPosition = MoveTowardsCustom(currentPosition, physicsPosition, delta, sqrDistance, distance, positionStep * deltaTime); // smoothly interpolate to the target rotation. // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. - Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime); + // Quaternions always need to be normalized in order to be a valid rotation after operations + Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime).normalized; // assign position and rotation together. faster than accessing manually. tf.SetPositionAndRotation(newPosition, newRotation); } - // creater visual copy only on clients, where players are watching. - public override void OnStartClient() + // simple and slow version with MoveTowards, which recalculates delta and delta.sqrMagnitude: + // Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * deltaTime); + // faster version copied from MoveTowards: + // this increases Prediction Benchmark Client's FPS from 615 -> 640. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector3 MoveTowardsCustom( + Vector3 current, + Vector3 target, + Vector3 _delta, // pass this in since we already calculated it + float _sqrDistance, // pass this in since we already calculated it + float _distance, // pass this in since we already calculated it + float maxDistanceDelta) { - // OnDeserialize may have already created this - if (physicsCopy == null) CreateGhosts(); + if (_sqrDistance == 0.0 || maxDistanceDelta >= 0.0 && _sqrDistance <= maxDistanceDelta * maxDistanceDelta) + return target; + + float distFactor = maxDistanceDelta / _distance; // unlike Vector3.MoveTowards, we only calculate this once + return new Vector3( + // current.x + (_delta.x / _distance) * maxDistanceDelta, + // current.y + (_delta.y / _distance) * maxDistanceDelta, + // current.z + (_delta.z / _distance) * maxDistanceDelta); + current.x + _delta.x * distFactor, + current.y + _delta.y * distFactor, + current.z + _delta.z * distFactor); } // destroy visual copy only in OnStopClient(). @@ -297,35 +395,158 @@ public override void OnStopClient() void UpdateServer() { - // to save bandwidth, we only serialize when position changed - // if (Vector3.Distance(tf.position, lastPosition) >= positionSensitivity) - // { - // lastPosition = tf.position; - // SetDirty(); - // } + // bandwidth optimization while idle. + if (reduceSendsWhileIdle) + { + // while moving, always sync every frame for immediate corrections. + // while idle, only sync once per second. + // + // we still need to sync occasionally because objects on client + // may still slide or move slightly due to gravity, physics etc. + // and those still need to get corrected if not moving on server. + // + // TODO + // next round of optimizations: if client received nothing for 1s, + // force correct to last received state. then server doesn't need + // to send once per second anymore. + syncInterval = IsMoving() ? 0 : 1; + } - // always set dirty to always serialize. - // fixes issues where an object was idle and stopped serializing on server, - // even though it was still moving on client. - // hence getting totally out of sync. + // always set dirty to always serialize in next sync interval. SetDirty(); } + // movement detection is virtual, in case projects want to use other methods. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool IsMoving() => + // straight forward implementation + // predictedRigidbody.velocity.magnitude >= motionSmoothingVelocityThreshold || + // predictedRigidbody.angularVelocity.magnitude >= motionSmoothingAngularVelocityThreshold; + // faster implementation with cached ² + predictedRigidbody.velocity.sqrMagnitude >= motionSmoothingVelocityThresholdSqr || + predictedRigidbody.angularVelocity.sqrMagnitude >= motionSmoothingAngularVelocityThresholdSqr; + + // TODO maybe merge the IsMoving() checks & callbacks with UpdateState(). + void UpdateGhosting() + { + // perf: enough to check ghosts every few frames. + // PredictionBenchmark: only checking every 4th frame: 585 => 600 FPS + if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; + + // client only uses ghosts on demand while interacting. + // this way 1000 GameObjects don't need +1000 Ghost GameObjects all the time! + + // no ghost at the moment + if (physicsCopy == null) + { + // faster than velocity threshold? then create the ghosts. + // with 10% buffer zone so we don't flip flop all the time. + if (IsMoving()) + { + CreateGhosts(); + OnBeginPrediction(); + } + } + // ghosting at the moment + else + { + // always set last moved time while moving. + // this way we can avoid on/off/oneffects when stopping. + if (IsMoving()) + { + motionSmoothingLastMovedTime = NetworkTime.time; + } + // slower than velocity threshold? then destroy the ghosts. + // with a minimum time since starting to move, to avoid on/off/on effects. + else + { + if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) + { + DestroyGhosts(); + OnEndPrediction(); + physicsCopy = null; // TESTING + } + } + } + } + + // when using Fast mode, we don't create any ghosts. + // but we still want to check IsMoving() in order to support the same + // user callbacks. + bool lastMoving = false; + void UpdateState() + { + // perf: enough to check ghosts every few frames. + // PredictionBenchmark: only checking every 4th frame: 770 => 800 FPS + if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; + + bool moving = IsMoving(); + + // started moving? + if (moving && !lastMoving) + { + OnBeginPrediction(); + lastMoving = true; + } + // stopped moving? + else if (!moving && lastMoving) + { + // ensure a minimum time since starting to move, to avoid on/off/on effects. + if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) + { + OnEndPrediction(); + lastMoving = false; + } + } + } + void Update() { if (isServer) UpdateServer(); + if (isClientOnly) + { + if (mode == PredictionMode.Smooth) + UpdateGhosting(); + else if (mode == PredictionMode.Fast) + UpdateState(); + } } void LateUpdate() { - if (isClient) SmoothFollowPhysicsCopy(); + // only follow on client-only, not in server or host mode + if (isClientOnly && mode == PredictionMode.Smooth && physicsCopy) SmoothFollowPhysicsCopy(); } void FixedUpdate() { - // on clients we record the current state every FixedUpdate. + // on clients (not host) we record the current state every FixedUpdate. // this is cheap, and allows us to keep a dense history. - if (isClient) RecordState(); + if (!isClientOnly) return; + + // OPTIMIZATION: RecordState() is expensive because it inserts into a SortedList. + // only record if state actually changed! + // risks not having up to date states when correcting, + // but it doesn't matter since we'll always compare with the 'newest' anyway. + // + // we check in here instead of in RecordState() because RecordState() should definitely record if we call it! + if (onlyRecordChanges) + { + // TODO maybe don't reuse the correction thresholds? + tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); + // clean & simple: + // if (Vector3.Distance(lastRecorded.position, position) < positionCorrectionThreshold && + // Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold) + // faster: + if ((lastRecorded.position - position).sqrMagnitude < positionCorrectionThresholdSqr && + Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold) + { + // Debug.Log($"FixedUpdate for {name}: taking optimized early return instead of recording state."); + return; + } + } + + RecordState(); } // manually store last recorded so we can easily check against this @@ -334,10 +555,13 @@ void FixedUpdate() double lastRecordTime; void RecordState() { + // performance optimization: only call NetworkTime.time getter once + double networkTime = NetworkTime.time; + // instead of recording every fixedupdate, let's record in an interval. // we don't want to record every tiny move and correct too hard. - if (NetworkTime.time < lastRecordTime + recordInterval) return; - lastRecordTime = NetworkTime.time; + if (networkTime < lastRecordTime + recordInterval) return; + lastRecordTime = networkTime; // NetworkTime.time is always behind by bufferTime. // prediction aims to be on the exact same server time (immediately). @@ -356,27 +580,42 @@ void RecordState() if (stateHistory.Count >= stateHistoryLimit) stateHistory.RemoveAt(0); + // grab current position/rotation/velocity only once. + // this is performance critical, avoid calling .transform multiple times. + tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than accessing .position + .rotation manually + Vector3 currentVelocity = predictedRigidbody.velocity; + Vector3 currentAngularVelocity = predictedRigidbody.angularVelocity; + // calculate delta to previous state (if any) Vector3 positionDelta = Vector3.zero; Vector3 velocityDelta = Vector3.zero; + Vector3 angularVelocityDelta = Vector3.zero; Quaternion rotationDelta = Quaternion.identity; - if (stateHistory.Count > 0) + int stateHistoryCount = stateHistory.Count; // perf: only grab .Count once + if (stateHistoryCount > 0) { - RigidbodyState last = stateHistory.Values[stateHistory.Count - 1]; - positionDelta = physicsCopyRigidbody.position - last.position; - velocityDelta = physicsCopyRigidbody.velocity - last.velocity; - rotationDelta = physicsCopyRigidbody.rotation * Quaternion.Inverse(last.rotation); // this is how you calculate a quaternion delta + RigidbodyState last = stateHistory.Values[stateHistoryCount - 1]; + positionDelta = currentPosition - last.position; + velocityDelta = currentVelocity - last.velocity; + // Quaternions always need to be normalized in order to be valid rotations after operations + rotationDelta = (currentRotation * Quaternion.Inverse(last.rotation)).normalized; + angularVelocityDelta = currentAngularVelocity - last.angularVelocity; // debug draw the recorded state - Debug.DrawLine(last.position, physicsCopyRigidbody.position, Color.red, lineTime); + // Debug.DrawLine(last.position, currentPosition, Color.red, lineTime); } // create state to insert RigidbodyState state = new RigidbodyState( predictedTime, - positionDelta, physicsCopyRigidbody.position, - rotationDelta, physicsCopyRigidbody.rotation, - velocityDelta, physicsCopyRigidbody.velocity + positionDelta, + currentPosition, + rotationDelta, + currentRotation, + velocityDelta, + currentVelocity, + angularVelocityDelta, + currentAngularVelocity ); // add state to history @@ -388,33 +627,48 @@ void RecordState() // optional user callbacks, in case people need to know about events. protected virtual void OnSnappedIntoPlace() {} + protected virtual void OnBeforeApplyState() {} protected virtual void OnCorrected() {} + protected virtual void OnBeginPrediction() {} // when the Rigidbody moved above threshold and we created a ghost + protected virtual void OnEndPrediction() {} // when the Rigidbody came to rest and we destroyed the ghost - void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity) + void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity) { // fix rigidbodies seemingly dancing in place instead of coming to rest. // hard snap to the position below a threshold velocity. // this is fine because the visual object still smoothly interpolates to it. - if (physicsCopyRigidbody.velocity.magnitude <= snapThreshold) + // => consider both velocity and angular velocity (in case of Rigidbodies only rotating with joints etc.) + if (predictedRigidbody.velocity.magnitude <= snapThreshold && + predictedRigidbody.angularVelocity.magnitude <= snapThreshold) { - // Debug.Log($"Prediction: snapped {name} into place because velocity {physicsCopyRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); + // Debug.Log($"Prediction: snapped {name} into place because velocity {predictedRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); // apply server state immediately. // important to apply velocity as well, instead of Vector3.zero. // in case an object is still slightly moving, we don't want it // to stop and start moving again on client - slide as well here. - physicsCopyRigidbody.position = position; - physicsCopyRigidbody.rotation = rotation; - physicsCopyRigidbody.velocity = velocity; + predictedRigidbody.position = position; + predictedRigidbody.rotation = rotation; + // projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error + if (!predictedRigidbody.isKinematic) + { + predictedRigidbody.velocity = velocity; + predictedRigidbody.angularVelocity = angularVelocity; + } // clear history and insert the exact state we just applied. // this makes future corrections more accurate. stateHistory.Clear(); stateHistory.Add(timestamp, new RigidbodyState( timestamp, - Vector3.zero, position, - Quaternion.identity, rotation, - Vector3.zero, velocity + Vector3.zero, + position, + Quaternion.identity, + rotation, + Vector3.zero, + velocity, + Vector3.zero, + angularVelocity )); // user callback @@ -422,35 +676,63 @@ void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 return; } - // Rigidbody .position teleports, while .MovePosition interpolates - // TODO is this a good idea? what about next capture while it's interpolating? - if (correctionMode == CorrectionMode.Move) + // we have a callback for snapping into place (above). + // we also need one for corrections without snapping into place. + // call it before applying pos/rot/vel in case we need to set kinematic etc. + OnBeforeApplyState(); + + // apply the state to the Rigidbody + if (mode == PredictionMode.Smooth) { - physicsCopyRigidbody.MovePosition(position); - physicsCopyRigidbody.MoveRotation(rotation); + // Smooth mode separates Physics from Renderering. + // Rendering smoothly follows Physics in SmoothFollowPhysicsCopy(). + // this allows us to be able to hard teleport to the correction. + // which gives most accurate results since the Rigidbody can't + // be stopped by another object when trying to correct. + predictedRigidbody.position = position; + predictedRigidbody.rotation = rotation; } - else if (correctionMode == CorrectionMode.Set) + else if (mode == PredictionMode.Fast) { - physicsCopyRigidbody.position = position; - physicsCopyRigidbody.rotation = rotation; + // Fast mode doesn't separate physics from rendering. + // The only smoothing we get is from Rigidbody.MovePosition. + predictedRigidbody.MovePosition(position); + predictedRigidbody.MoveRotation(rotation); } - // there's only one way to set velocity - physicsCopyRigidbody.velocity = velocity; + // there's only one way to set velocity. + // (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error) + if (!predictedRigidbody.isKinematic) + { + predictedRigidbody.velocity = velocity; + predictedRigidbody.angularVelocity = angularVelocity; + } } // process a received server state. // compares it against our history and applies corrections if needed. - void OnReceivedState(double timestamp, RigidbodyState state) + void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping) { // always update remote state ghost if (remoteCopy != null) { - remoteCopy.transform.position = state.position; - remoteCopy.transform.rotation = state.rotation; - remoteCopy.transform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment. + Transform remoteCopyTransform = remoteCopy.transform; + remoteCopyTransform.SetPositionAndRotation(state.position, state.rotation); // faster than .position + .rotation setters + remoteCopyTransform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment. } + + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // color code remote sleeping objects to debug objects coming to rest + // if (showRemoteSleeping) + // { + // rend.material.color = sleeping ? Color.gray : originalColor; + // } + + // performance: get Rigidbody position & rotation only once, + // and together via its transform + predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); + // OPTIONAL performance optimization when comparing idle objects. // even idle objects will have a history of ~32 entries. // sampling & traversing through them is unnecessarily costly. @@ -469,9 +751,11 @@ void OnReceivedState(double timestamp, RigidbodyState state) // this is as fast as it gets for skipping idle objects. // // if this ever causes issues, feel free to disable it. + float positionToStateDistanceSqr = Vector3.SqrMagnitude(state.position - physicsPosition); if (compareLastFirst && - Vector3.Distance(state.position, physicsCopyRigidbody.position) < positionCorrectionThreshold && - Quaternion.Angle(state.rotation, physicsCopyRigidbody.rotation) < rotationCorrectionThreshold) + // Vector3.Distance(state.position, physicsPosition) < positionCorrectionThreshold && // slow comparison + positionToStateDistanceSqr < positionCorrectionThresholdSqr && // fast comparison + Quaternion.Angle(state.rotation, physicsRotation) < rotationCorrectionThreshold) { // Debug.Log($"OnReceivedState for {name}: taking optimized early return!"); return; @@ -498,8 +782,14 @@ void OnReceivedState(double timestamp, RigidbodyState state) // otherwise it could be out of sync as long as it's too far behind. if (state.timestamp < oldest.timestamp) { - Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind."); - ApplyState(state.timestamp, state.position, state.rotation, state.velocity); + // when starting, client may only have 2-3 states in history. + // it's expected that server states would be behind those 2-3. + // only show a warning if it's behind the full history limit! + if (stateHistory.Count >= stateHistoryLimit) + Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind."); + + // force apply the state + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); return; } @@ -516,11 +806,13 @@ void OnReceivedState(double timestamp, RigidbodyState state) // we clamp it to 'now'. // but only correct if off by threshold. // TODO maybe we should interpolate this back to 'now'? - if (Vector3.Distance(state.position, physicsCopyRigidbody.position) >= positionCorrectionThreshold) + // if (Vector3.Distance(state.position, physicsPosition) >= positionCorrectionThreshold) // slow comparison + if (positionToStateDistanceSqr >= positionCorrectionThresholdSqr) // fast comparison { - double ahead = state.timestamp - newest.timestamp; - Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); - ApplyState(state.timestamp, state.position, state.rotation, state.velocity); + // this can happen a lot when latency is ~0. logging all the time allocates too much and is too slow. + // double ahead = state.timestamp - newest.timestamp; + // Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); } return; } @@ -531,7 +823,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // something went very wrong. sampling should've worked. // hard correct to recover the error. Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history."); - ApplyState(state.timestamp, state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); return; } @@ -540,19 +832,21 @@ void OnReceivedState(double timestamp, RigidbodyState state) // calculate the difference between where we were and where we should be // TODO only position for now. consider rotation etc. too later - float positionDifference = Vector3.Distance(state.position, interpolated.position); - float rotationDifference = Quaternion.Angle(state.rotation, interpolated.rotation); + // float positionToInterpolatedDistance = Vector3.Distance(state.position, interpolated.position); // slow comparison + float positionToInterpolatedDistanceSqr = Vector3.SqrMagnitude(state.position - interpolated.position); // fast comparison + float rotationToInterpolatedDistance = Quaternion.Angle(state.rotation, interpolated.rotation); // Debug.Log($"Sampled history of size={stateHistory.Count} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3} / {correctionThreshold:F3}"); // too far off? then correct it - if (positionDifference >= positionCorrectionThreshold || - rotationDifference >= rotationCorrectionThreshold) + if (positionToInterpolatedDistanceSqr >= positionCorrectionThresholdSqr || // fast comparison + //positionToInterpolatedDistance >= positionCorrectionThreshold || // slow comparison + rotationToInterpolatedDistance >= rotationCorrectionThreshold) { // Debug.Log($"CORRECTION NEEDED FOR {name} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3}"); // show the received correction position + velocity for debugging. // helps to compare with the interpolated/applied correction locally. - Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime); + //Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime); // insert the correction and correct the history on top of it. // returns the final recomputed state after rewinding. @@ -563,8 +857,8 @@ void OnReceivedState(double timestamp, RigidbodyState state) // for example, on same machine with near zero latency. // int correctedAmount = stateHistory.Count - afterIndex; // Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); - Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); - ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity); + //Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); + ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity); // user callback OnCorrected(); @@ -585,27 +879,57 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) // server is technically supposed to be at a fixed frame rate, but this can vary. // sending server's current deltaTime is the safest option. // client then applies it on top of remoteTimestamp. - writer.WriteFloat(Time.deltaTime); - writer.WriteVector3(rb.position); // own rigidbody on server, it's never moved to physics copy - writer.WriteQuaternion(rb.rotation); // own rigidbody on server, it's never moved to physics copy - writer.WriteVector3(rb.velocity); // own rigidbody on server, it's never moved to physics copy + + + // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! + tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); // faster than tf.position + tf.rotation. server's rigidbody is on the same transform. + + // simple but slow write: + // writer.WriteFloat(Time.deltaTime); + // writer.WriteVector3(position); + // writer.WriteQuaternion(rotation); + // writer.WriteVector3(predictedRigidbody.velocity); + // writer.WriteVector3(predictedRigidbody.angularVelocity); + + // performance optimization: write a whole struct at once via blittable: + PredictedSyncData data = new PredictedSyncData( + Time.deltaTime, + position, + rotation, + predictedRigidbody.velocity, + predictedRigidbody.angularVelocity);//, + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // predictedRigidbody.IsSleeping()); + writer.WritePredictedSyncData(data); } // read the server's state, compare with client state & correct if necessary. public override void OnDeserialize(NetworkReader reader, bool initialState) { - // this may be called before OnStartClient. - // in that case, separate physics first before applying state. - if (physicsCopy == null) CreateGhosts(); - // deserialize data // we want to know the time on the server when this was sent, which is remoteTimestamp. double timestamp = NetworkClient.connection.remoteTimeStamp; - // server send state at the end of the frame. + // simple but slow read: + // double serverDeltaTime = reader.ReadFloat(); + // Vector3 position = reader.ReadVector3(); + // Quaternion rotation = reader.ReadQuaternion(); + // Vector3 velocity = reader.ReadVector3(); + // Vector3 angularVelocity = reader.ReadVector3(); + + // performance optimization: read a whole struct at once via blittable: + PredictedSyncData data = reader.ReadPredictedSyncData(); + double serverDeltaTime = data.deltaTime; + Vector3 position = data.position; + Quaternion rotation = data.rotation; + Vector3 velocity = data.velocity; + Vector3 angularVelocity = data.angularVelocity; + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // bool sleeping = data.sleeping != 0; + + // server sends state at the end of the frame. // parse and apply the server's delta time to our timestamp. // otherwise we see noticeable resets that seem off by one frame. - double serverDeltaTime = reader.ReadFloat(); timestamp += serverDeltaTime; // however, adding yet one more frame delay gives much(!) better results. @@ -614,13 +938,8 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) // with physics happening at the end of the frame? if (oneFrameAhead) timestamp += serverDeltaTime; - // parse state - Vector3 position = reader.ReadVector3(); - Quaternion rotation = reader.ReadQuaternion(); - Vector3 velocity = reader.ReadVector3(); - // process received state - OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, Quaternion.identity, rotation, Vector3.zero, velocity)); + OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, Quaternion.identity, rotation, Vector3.zero, velocity, Vector3.zero, angularVelocity));//, sleeping); } protected override void OnValidate() @@ -635,5 +954,44 @@ protected override void OnValidate() // then we can maybe relax this a bit. syncInterval = 0; } + + // helper function for Physics tests to check if a Rigidbody belongs to + // a PredictedRigidbody component (either on it, or on its ghost). + public static bool IsPredicted(Rigidbody rb, out PredictedRigidbody predictedRigidbody) + { + // by default, Rigidbody is on the PredictedRigidbody GameObject + if (rb.TryGetComponent(out predictedRigidbody)) + return true; + + // it might be on a ghost while interacting + if (rb.TryGetComponent(out PredictedRigidbodyPhysicsGhost ghost)) + { + predictedRigidbody = ghost.target.GetComponent(); + return true; + } + + // otherwise the Rigidbody does not belong to any PredictedRigidbody. + predictedRigidbody = null; + return false; + } + + // helper function for Physics tests to check if a Collider (which may be in children) belongs to + // a PredictedRigidbody component (either on it, or on its ghost). + public static bool IsPredicted(Collider co, out PredictedRigidbody predictedRigidbody) + { + // by default, Collider is on the PredictedRigidbody GameObject or it's children. + predictedRigidbody = co.GetComponentInParent(); + if (predictedRigidbody != null) + return true; + + // it might be on a ghost while interacting + PredictedRigidbodyPhysicsGhost ghost = co.GetComponentInParent(); + if (ghost != null && ghost.target != null && ghost.target.TryGetComponent(out predictedRigidbody)) + return true; + + // otherwise the Rigidbody does not belong to any PredictedRigidbody. + predictedRigidbody = null; + return false; + } } } diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs index 3168f15bd..f28d49fae 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs @@ -1,6 +1,6 @@ // Prediction moves out the Rigidbody & Collider into a separate object. -// This way the main (visual) object can smoothly follow it, instead of hard. -using System; +// this component simply points back to the owner component. +// in case Raycasts hit it and need to know the owner, etc. using UnityEngine; namespace Mirror @@ -11,66 +11,5 @@ public class PredictedRigidbodyPhysicsGhost : MonoBehaviour // PredictedRigidbody, this way we don't need to call the .transform getter. [Tooltip("The predicted rigidbody owner.")] public Transform target; - - // ghost (settings are copyed from PredictedRigidbody) - MeshRenderer ghost; - public float ghostDistanceThreshold = 0.1f; - public float ghostEnabledCheckInterval = 0.2f; - double lastGhostEnabledCheckTime = 0; - - // cache components because this is performance critical! - Transform tf; - Collider co; - - // we add this component manually from PredictedRigidbody. - // so assign this in Start. target isn't set in Awake yet. - void Start() - { - tf = transform; - co = GetComponent(); - ghost = GetComponent(); - } - - void UpdateGhostRenderers() - { - // only if a ghost renderer was given - if (ghost == null) return; - - // enough to run this in a certain interval. - // doing this every update would be overkill. - // this is only for debug purposes anyway. - if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return; - lastGhostEnabledCheckTime = NetworkTime.localTime; - - // only show ghost while interpolating towards the object. - // if we are 'inside' the object then don't show ghost. - // otherwise it just looks like z-fighting the whole time. - // => iterated the renderers we found when creating the visual copy. - // we don't want to GetComponentsInChildren every time here! - bool insideTarget = Vector3.Distance(tf.position, target.position) <= ghostDistanceThreshold; - ghost.enabled = !insideTarget; - } - - void Update() => UpdateGhostRenderers(); - - // always follow in late update, after update modified positions - void LateUpdate() - { - // if owner gets network destroyed for any reason, destroy visual - if (target == null) Destroy(gameObject); - } - - // also show a yellow gizmo for the predicted & corrected physics. - // in case we can't renderer ghosts, at least we have this. - void OnDrawGizmos() - { - if (co != null) - { - // show the client's predicted & corrected physics in yellow - Bounds bounds = co.bounds; - Gizmos.color = Color.yellow; - Gizmos.DrawWireCube(bounds.center, bounds.size); - } - } } } diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs index 4e1127c38..636f39702 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs @@ -1,59 +1 @@ -// simply ghost object that always follows last received server state. -using UnityEngine; - -namespace Mirror -{ - public class PredictedRigidbodyRemoteGhost : MonoBehaviour - { - // this is performance critical, so store target's .Transform instead of - // PredictedRigidbody, this way we don't need to call the .transform getter. - [Tooltip("The predicted rigidbody owner.")] - public Transform target; - - // ghost (settings are copyed from PredictedRigidbody) - MeshRenderer ghost; - public float ghostDistanceThreshold = 0.1f; - public float ghostEnabledCheckInterval = 0.2f; - double lastGhostEnabledCheckTime = 0; - - // cache components because this is performance critical! - Transform tf; - - // we add this component manually from PredictedRigidbody. - // so assign this in Start. target isn't set in Awake yet. - void Start() - { - tf = transform; - ghost = GetComponent(); - } - - void UpdateGhostRenderers() - { - // only if a ghost renderer was given - if (ghost == null) return; - - // enough to run this in a certain interval. - // doing this every update would be overkill. - // this is only for debug purposes anyway. - if (NetworkTime.localTime < lastGhostEnabledCheckTime + ghostEnabledCheckInterval) return; - lastGhostEnabledCheckTime = NetworkTime.localTime; - - // only show ghost while interpolating towards the object. - // if we are 'inside' the object then don't show ghost. - // otherwise it just looks like z-fighting the whole time. - // => iterated the renderers we found when creating the visual copy. - // we don't want to GetComponentsInChildren every time here! - bool insideTarget = Vector3.Distance(tf.position, target.position) <= ghostDistanceThreshold; - ghost.enabled = !insideTarget; - } - - void Update() => UpdateGhostRenderers(); - - // always follow in late update, after update modified positions - void LateUpdate() - { - // if owner gets network destroyed for any reason, destroy visual - if (target == null) Destroy(gameObject); - } - } -} +// removed 2024-02-09 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs new file mode 100644 index 000000000..fa652fd14 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs @@ -0,0 +1,54 @@ +// this struct exists only for OnDe/Serialize performance. +// instead of WriteVector3+Quaternion+Vector3+Vector3, +// we read & write the whole struct as blittable once. +// +// struct packing can cause odd results with blittable on different platforms, +// so this is usually not recommended! +// +// in this case however, we need to squeeze everything we can out of prediction +// to support low even devices / VR. +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Mirror +{ + // struct packing + + [StructLayout(LayoutKind.Sequential)] // explicitly force sequential + public struct PredictedSyncData + { + public float deltaTime; // 4 bytes (word aligned) + public Vector3 position; // 12 bytes (word aligned) + public Quaternion rotation; // 16 bytes (word aligned) + public Vector3 velocity; // 12 bytes (word aligned) + public Vector3 angularVelocity; // 12 bytes (word aligned) + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // public byte sleeping; // 1 byte: bool isn't blittable + + // constructor for convenience + public PredictedSyncData(float deltaTime, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)//, bool sleeping) + { + this.deltaTime = deltaTime; + this.position = position; + this.rotation = rotation; + this.velocity = velocity; + this.angularVelocity = angularVelocity; + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // this.sleeping = sleeping ? (byte)1 : (byte)0; + } + } + + // NetworkReader/Writer extensions to write this struct + public static class PredictedSyncDataReadWrite + { + public static void WritePredictedSyncData(this NetworkWriter writer, PredictedSyncData data) + { + writer.WriteBlittable(data); + } + + public static PredictedSyncData ReadPredictedSyncData(this NetworkReader reader) + { + return reader.ReadBlittable(); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta new file mode 100644 index 000000000..f78b78d6c --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f595f112a39e4634b670d56991b23823 +timeCreated: 1710387026 \ No newline at end of file diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs index 3fd751624..9dc2b51d2 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs @@ -30,9 +30,19 @@ public static void MoveRigidbody(GameObject source, GameObject destination) rigidbodyCopy.constraints = original.constraints; rigidbodyCopy.sleepThreshold = original.sleepThreshold; rigidbodyCopy.freezeRotation = original.freezeRotation; - rigidbodyCopy.position = original.position; - rigidbodyCopy.rotation = original.rotation; - rigidbodyCopy.velocity = original.velocity; + + // moving (Configurable)Joints messes up their range of motion unless + // we reset to initial position first (we do this in PredictedRigibody.cs). + // so here we don't set the Rigidbody's physics position at all. + // rigidbodyCopy.position = original.position; + // rigidbodyCopy.rotation = original.rotation; + + // projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error + if (!original.isKinematic) + { + rigidbodyCopy.velocity = original.velocity; + rigidbodyCopy.angularVelocity = original.angularVelocity; + } // destroy original GameObject.Destroy(original); @@ -131,6 +141,21 @@ public static void MoveMeshColliders(GameObject source, GameObject destination) MeshCollider[] sourceColliders = source.GetComponentsInChildren(); foreach (MeshCollider sourceCollider in sourceColliders) { + // when Models have Mesh->Read/Write disabled, it means that Unity + // uploads the mesh directly to the GPU and erases it on the CPU. + // on some platforms this makes moving a MeshCollider in builds impossible: + // + // "CollisionMeshData couldn't be created because the mesh has been marked as non-accessible." + // + // on other platforms, this works fine. + // let's show an explicit log message so in case collisions don't + // work at runtime, it's obvious why it happens and how to fix it. + if (!sourceCollider.sharedMesh.isReadable) + { + Debug.Log($"[Prediction]: MeshCollider on {sourceCollider.name} isn't readable, which may indicate that the Mesh only exists on the GPU. If {sourceCollider.name} is missing collisions, then please select the model in the Project Area, and enable Mesh->Read/Write so it's also available on the CPU!"); + // don't early return. keep trying, it may work. + } + // copy the relative transform: // if collider is on root, it returns destination root. // if collider is on a child, it creates and returns a child on destination. @@ -229,10 +254,10 @@ public static void MoveConfigurableJoints(GameObject source, GameObject destinat jointCopy.connectedMassScale = sourceJoint.connectedMassScale; jointCopy.enableCollision = sourceJoint.enableCollision; jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; - jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; + jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs jointCopy.linearLimitSpring = sourceJoint.linearLimitSpring; jointCopy.linearLimit = sourceJoint.linearLimit; - jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; + jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs jointCopy.massScale = sourceJoint.massScale; jointCopy.projectionAngle = sourceJoint.projectionAngle; jointCopy.projectionDistance = sourceJoint.projectionDistance; diff --git a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs index df4e25181..c30ce4a0c 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs @@ -1,28 +1,38 @@ // PredictedRigidbody stores a history of its rigidbody states. +using System.Runtime.CompilerServices; using UnityEngine; namespace Mirror { + // inline everything because this is performance critical! public struct RigidbodyState : PredictedState { - public double timestamp { get; private set; } + public double timestamp { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } // we want to store position delta (last + delta = current), and current. // this way we can apply deltas on top of corrected positions to get the corrected final position. - public Vector3 positionDelta { get; set; } // delta to get from last to this position - public Vector3 position { get; set; } + public Vector3 positionDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this position + public Vector3 position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } - public Quaternion rotationDelta { get; set; } // delta to get from last to this rotation - public Quaternion rotation { get; set; } + public Quaternion rotationDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this rotation + public Quaternion rotation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } - public Vector3 velocityDelta { get; set; } // delta to get from last to this velocity - public Vector3 velocity { get; set; } + public Vector3 velocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity + public Vector3 velocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Vector3 angularVelocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity + public Vector3 angularVelocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } public RigidbodyState( double timestamp, - Vector3 positionDelta, Vector3 position, - Quaternion rotationDelta, Quaternion rotation, - Vector3 velocityDelta, Vector3 velocity) + Vector3 positionDelta, + Vector3 position, + Quaternion rotationDelta, + Quaternion rotation, + Vector3 velocityDelta, + Vector3 velocity, + Vector3 angularVelocityDelta, + Vector3 angularVelocity) { this.timestamp = timestamp; this.positionDelta = positionDelta; @@ -31,6 +41,8 @@ public RigidbodyState( this.rotation = rotation; this.velocityDelta = velocityDelta; this.velocity = velocity; + this.angularVelocityDelta = angularVelocityDelta; + this.angularVelocity = angularVelocity; } public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t) @@ -38,8 +50,10 @@ public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, flo return new RigidbodyState { position = Vector3.Lerp(a.position, b.position, t), - rotation = Quaternion.Slerp(a.rotation, b.rotation, t), - velocity = Vector3.Lerp(a.velocity, b.velocity, t) + // Quaternions always need to be normalized in order to be a valid rotation after operations + rotation = Quaternion.Slerp(a.rotation, b.rotation, t).normalized, + velocity = Vector3.Lerp(a.velocity, b.velocity, t), + angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t) }; } } diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs index aa1881563..5b3ede9cd 100644 --- a/Assets/Mirror/Components/RemoteStatistics.cs +++ b/Assets/Mirror/Components/RemoteStatistics.cs @@ -91,7 +91,7 @@ public class RemoteStatistics : NetworkBehaviour [Header("GUI")] public bool showGui; - public KeyCode hotKey = KeyCode.F11; + public KeyCode hotKey = KeyCode.BackQuote; Rect windowRect = new Rect(0, 0, 400, 400); // password can't be stored in code or in Unity project. diff --git a/Assets/Mirror/Core/AssemblyInfo.cs b/Assets/Mirror/Core/AssemblyInfo.cs index f342716a8..a9c64421b 100644 --- a/Assets/Mirror/Core/AssemblyInfo.cs +++ b/Assets/Mirror/Core/AssemblyInfo.cs @@ -10,3 +10,4 @@ [assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")] [assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")] [assembly: InternalsVisibleTo("Mirror.Editor")] +[assembly: InternalsVisibleTo("Mirror.Components")] diff --git a/Assets/Mirror/Core/Attributes.cs b/Assets/Mirror/Core/Attributes.cs index df8236a8f..0aebfbabe 100644 --- a/Assets/Mirror/Core/Attributes.cs +++ b/Assets/Mirror/Core/Attributes.cs @@ -4,8 +4,12 @@ namespace Mirror { /// - /// SyncVars are used to synchronize a variable from the server to all clients automatically. - /// Value must be changed on server, not directly by clients. Hook parameter allows you to define a client-side method to be invoked when the client gets an update from the server. + /// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default. + /// + /// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients. + /// Otherwise, the value should be changed on the client side and synchronized to server and other clients. + /// + /// Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side. /// [AttributeUsage(AttributeTargets.Field)] public class SyncVarAttribute : PropertyAttribute diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index 9240d7ffc..cc81b9423 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -135,8 +135,9 @@ public bool authority // -> still supports dynamically sized types // // 64 bit mask, tracking up to 64 SyncVars. - protected ulong syncVarDirtyBits { get; private set; } - // 64 bit mask, tracking up to 64 sync collections (internal for tests). + // protected since NB child classes read this field in the weaver generated SerializeSyncVars method + protected ulong syncVarDirtyBits; + // 64 bit mask, tracking up to 64 sync collections. // internal for tests, field for faster access (instead of property) // TODO 64 SyncLists are too much. consider smaller mask later. internal ulong syncObjectDirtyBits; diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index 479210877..097dd3e68 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -261,7 +261,8 @@ static void OnTransportConnected() // the handler may want to send messages to the client // thus we should set the connected state before calling the handler connectState = ConnectState.Connected; - NetworkTime.UpdateClient(); + // ping right away after connecting so client gets new time asap + NetworkTime.SendPing(); OnConnectedEvent?.Invoke(); } else Debug.LogError("Skipped Connect message handling because connection is null."); diff --git a/Assets/Mirror/Core/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs index 41c52c47f..f8d92cbec 100644 --- a/Assets/Mirror/Core/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -38,7 +38,7 @@ public class NetworkManager : MonoBehaviour public bool editorAutoStart; /// 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 & Client send rate per second. Use 60-100Hz 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 / Client send rate per second.\nUse 60-100Hz for fast paced games like Counter-Strike to minimize latency.\nUse around 30Hz for games like WoW to minimize computations.\nUse around 1-10Hz for slow paced games like EVE.")] [FormerlySerializedAs("serverTickRate")] public int sendRate = 60; @@ -597,11 +597,9 @@ void FinishStartHost() // client will do things before the server is even fully started. //Debug.Log("StartHostClient called"); SetupClient(); - - networkAddress = "localhost"; RegisterClientMessages(); - // call OnConencted needs to be called AFTER RegisterClientMessages + // InvokeOnConnected needs to be called AFTER RegisterClientMessages // (https://github.com/vis2k/Mirror/pull/1249/) HostMode.InvokeOnConnected(); diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 547ba470c..7b5026583 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -266,21 +266,8 @@ static void CleanupSpawned() { if (identity != null) { - // scene object - if (identity.sceneId != 0) - { - // spawned scene objects are unspawned and reset. - // afterwards we disable them again. - // (they always stay in the scene, we don't destroy them) - DestroyObject(identity, DestroyMode.Reset); - identity.gameObject.SetActive(false); - } - // spawned prefabs - else - { - // spawned prefabs are unspawned and destroyed. - DestroyObject(identity, DestroyMode.Destroy); - } + // NetworkServer.Destroy resets if scene object, destroys if prefab. + Destroy(identity.gameObject); } } @@ -337,7 +324,7 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, // 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 {identity.name} netId={msg.netId}"); + Debug.LogWarning($"Spawned object not found when handling Command message netId={msg.netId}"); return; } @@ -385,7 +372,7 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta if (!identity.DeserializeServer(reader)) { if (exceptionsDisconnect) - { + { Debug.LogError($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); connection.Disconnect(); } @@ -928,22 +915,22 @@ public static void ReplaceHandler(Action handle where T : struct, NetworkMessage { ushort msgType = NetworkMessageId.Id; - + // register Id <> Type in lookup for debugging. NetworkMessages.Lookup[msgType] = typeof(T); - + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } - + /// Replace a handler for message type T. Most should require authentication. public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { ushort msgType = NetworkMessageId.Id; - + // register Id <> Type in lookup for debugging. NetworkMessages.Lookup[msgType] = typeof(T); - + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } @@ -1519,6 +1506,10 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) if (ownerConnection is LocalConnectionToClient) identity.isOwned = true; + // NetworkServer.Unspawn sets object as inactive. + // NetworkServer.Spawn needs to set them active again in case they were previously unspawned / inactive. + identity.gameObject.SetActive(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) @@ -1564,43 +1555,26 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) // Unlike when calling NetworkServer.Destroy(), on the server the object // will NOT be destroyed. This allows the server to re-use the object, // even spawn it again later. - public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset); - - // destroy ///////////////////////////////////////////////////////////// - // 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) + public static void UnSpawn(GameObject obj) { // Debug.Log($"DestroyObject instance:{identity.netId}"); - // NetworkServer.Destroy should only be called on server or host. + // NetworkServer.Unspawn should only be called on server or host. // on client, show a warning to explain what it does. if (!active) { - Debug.LogWarning("NetworkServer.Destroy() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); + Debug.LogWarning("NetworkServer.Unspawn() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); + return; + } + + if (obj == null) + { + Debug.Log("NetworkServer.Unspawn(): object is null"); + return; + } + + if (!GetNetworkIdentity(obj, out NetworkIdentity identity)) + { return; } @@ -1654,31 +1628,59 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // we are on the server. call OnStopServer. identity.OnStopServer(); - // are we supposed to GameObject.Destroy() it completely? - if (mode == DestroyMode.Destroy) + // finally reset the state and deactivate it + identity.ResetState(); + identity.gameObject.SetActive(false); + } + + // destroy ///////////////////////////////////////////////////////////// + /// 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) + { + // NetworkServer.Destroy should only be called on server or host. + // on client, show a warning to explain what it does. + if (!active) + { + Debug.LogWarning("NetworkServer.Destroy() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); + return; + } + + if (obj == null) + { + Debug.Log("NetworkServer.Destroy(): object is null"); + return; + } + + // first, we unspawn it on clients and server + UnSpawn(obj); + + // additionally, if it's a prefab then we destroy it completely. + // we never destroy scene objects on server or on client, since once + // they are gone, they are gone forever and can't be instantiate again. + // for example, server may Destroy() a scene object and once a match + // restarts, the scene objects would be gone from the new match. + if (GetNetworkIdentity(obj, out NetworkIdentity identity) && + identity.sceneId == 0) { identity.destroyCalled = true; // Destroy if application is running if (Application.isPlaying) { - UnityEngine.Object.Destroy(identity.gameObject); + UnityEngine.Object.Destroy(obj); } // Destroy can't be used in Editor during tests. use DestroyImmediate. else { - GameObject.DestroyImmediate(identity.gameObject); + GameObject.DestroyImmediate(obj); } } - // otherwise simply .Reset() and set inactive again - else if (mode == DestroyMode.Reset) - { - identity.ResetState(); - } } // interest management ///////////////////////////////////////////////// - // Helper function to add all server connections as observers. // This is used if none of the components provides their own // OnRebuildObservers function. diff --git a/Assets/Mirror/Core/NetworkTime.cs b/Assets/Mirror/Core/NetworkTime.cs index cb1f77762..6319970c4 100644 --- a/Assets/Mirror/Core/NetworkTime.cs +++ b/Assets/Mirror/Core/NetworkTime.cs @@ -141,17 +141,21 @@ internal static void UpdateClient() { // localTime (double) instead of Time.time for accuracy over days if (localTime >= lastPingTime + PingInterval) - { - // send raw predicted time without the offset applied yet. - // we then apply the offset to it after. - NetworkPingMessage pingMessage = new NetworkPingMessage - ( - localTime, - predictedTime - ); - NetworkClient.Send(pingMessage, Channels.Unreliable); - lastPingTime = localTime; - } + SendPing(); + } + + // Separate method so we can call it from NetworkClient directly. + internal static void SendPing() + { + // send raw predicted time without the offset applied yet. + // we then apply the offset to it after. + NetworkPingMessage pingMessage = new NetworkPingMessage + ( + localTime, + predictedTime + ); + NetworkClient.Send(pingMessage, Channels.Unreliable); + lastPingTime = localTime; } // client rtt calculation ////////////////////////////////////////////// diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs b/Assets/Mirror/Core/Prediction/Prediction.cs index 9a39ddd0e..d66994527 100644 --- a/Assets/Mirror/Core/Prediction/Prediction.cs +++ b/Assets/Mirror/Core/Prediction/Prediction.cs @@ -19,12 +19,16 @@ public interface PredictedState Vector3 velocity { get; set; } Vector3 velocityDelta { get; set; } + + Vector3 angularVelocity { get; set; } + Vector3 angularVelocityDelta { get; set; } } public static class Prediction { // get the two states closest to a given timestamp. // those can be used to interpolate the exact state at that time. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! public static bool Sample( SortedList history, double timestamp, // current server time @@ -56,29 +60,36 @@ public static bool Sample( // should be O(1) most of the time, unless sampling was off. int index = 0; // manually count when iterating. easier than for-int loop. KeyValuePair prev = new KeyValuePair(); - foreach (KeyValuePair entry in history) { + + // SortedList foreach iteration allocates a LOT. use for-int instead. + // foreach (KeyValuePair entry in history) { + for (int i = 0; i < history.Count; ++i) + { + double key = history.Keys[i]; + T value = history.Values[i]; + // exact match? - if (timestamp == entry.Key) + if (timestamp == key) { - before = entry.Value; - after = entry.Value; + before = value; + after = value; afterIndex = index; - t = Mathd.InverseLerp(entry.Key, entry.Key, timestamp); + t = Mathd.InverseLerp(key, key, timestamp); return true; } // did we check beyond timestamp? then return the previous two. - if (entry.Key > timestamp) + if (key > timestamp) { before = prev.Value; - after = entry.Value; + after = value; afterIndex = index; - t = Mathd.InverseLerp(prev.Key, entry.Key, timestamp); + t = Mathd.InverseLerp(prev.Key, key, timestamp); return true; } // remember the last - prev = entry; + prev = new KeyValuePair(key, value); index += 1; } @@ -88,22 +99,33 @@ public static bool Sample( // inserts a server state into the client's history. // readjust the deltas of the states after the inserted one. // returns the corrected final position. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! public static T CorrectHistory( - SortedList stateHistory, + SortedList history, int stateHistoryLimit, T corrected, // corrected state with timestamp T before, // state in history before the correction T after, // state in history after the correction - int afterIndex) // index of the 'after' value so we don't need to find it again here + int afterIndex) // index of the 'after' value so we don't need to find it again here where T: PredictedState { // respect the limit // TODO unit test to check if it respects max size - if (stateHistory.Count >= stateHistoryLimit) - stateHistory.RemoveAt(0); + if (history.Count >= stateHistoryLimit) + { + history.RemoveAt(0); + afterIndex -= 1; // we removed the first value so all indices are off by one now + } - // insert the corrected state into the history, or overwrite if already exists - stateHistory[corrected.timestamp] = corrected; + // PERFORMANCE OPTIMIZATION: avoid O(N) insertion, only readjust all values after. + // the end result is the same since after.delta and after.position are both recalculated. + // it's technically not correct if we were to reconstruct final position from 0..after..end but + // we never do, we only ever iterate from after..end! + // + // insert the corrected state into the history, or overwrite if already exists + // SortedList insertions are O(N)! + // history[corrected.timestamp] = corrected; + // afterIndex += 1; // we inserted the corrected value before the previous index // the entry behind the inserted one still has the delta from (before, after). // we need to correct it to (corrected, after). @@ -136,35 +158,34 @@ public static T CorrectHistory( double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25 // recalculate 'after.delta' with the multiplier - after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier); - after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); - // rotation deltas aren't working yet. instead, we apply the corrected rotation to all entries after the correction below. - // this at least syncs the rotations and looks quite decent, compared to not syncing! - // after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier); + after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier); + after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); + after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier); + // Quaternions always need to be normalized in order to be a valid rotation after operations + after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier).normalized; // changes aren't saved until we overwrite them in the history - stateHistory[after.timestamp] = after; + history[after.timestamp] = after; // second step: readjust all absolute values by rewinding client's delta moves on top of it. T last = corrected; - for (int i = afterIndex; i < stateHistory.Count; ++i) + for (int i = afterIndex; i < history.Count; ++i) { - double key = stateHistory.Keys[i]; - T entry = stateHistory.Values[i]; + double key = history.Keys[i]; + T value = history.Values[i]; // correct absolute position based on last + delta. - entry.position = last.position + entry.positionDelta; - entry.velocity = last.velocity + entry.velocityDelta; - // rotation deltas aren't working yet. instead, we apply the corrected rotation to all entries after the correction. - // this at least syncs the rotations and looks quite decent, compared to not syncing! - // entry.rotation = entry.rotationDelta * last.rotation; // quaternions add delta by multiplying in this order - entry.rotation = corrected.rotation; + value.position = last.position + value.positionDelta; + value.velocity = last.velocity + value.velocityDelta; + value.angularVelocity = last.angularVelocity + value.angularVelocityDelta; + // Quaternions always need to be normalized in order to be a valid rotation after operations + value.rotation = (value.rotationDelta * last.rotation).normalized; // quaternions add delta by multiplying in this order // save the corrected entry into history. - stateHistory[key] = entry; + history[key] = value; // save last - last = entry; + last = value; } // third step: return the final recomputed state. diff --git a/Assets/Mirror/Core/SyncDictionary.cs b/Assets/Mirror/Core/SyncDictionary.cs index f0f052156..d22d39531 100644 --- a/Assets/Mirror/Core/SyncDictionary.cs +++ b/Assets/Mirror/Core/SyncDictionary.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; @@ -5,13 +6,31 @@ namespace Mirror { public class SyncIDictionary : SyncObject, IDictionary, IReadOnlyDictionary { - public delegate void SyncDictionaryChanged(Operation op, TKey key, TValue item); + /// This is called after the item is added with TKey + public Action OnAdd; + + /// This is called after the item is changed with TKey. TValue is the OLD item + public Action OnSet; + + /// This is called after the item is removed with TKey. TValue is the OLD item + public Action OnRemove; + + /// This is called before the data is cleared + public Action OnClear; + + // Deprecated 2024-03-22 + [Obsolete("Use individual Actions, which pass OLD values where appropriate, instead.")] + public Action Callback; protected readonly IDictionary objects; + public SyncIDictionary(IDictionary objects) + { + this.objects = objects; + } + public int Count => objects.Count; public bool IsReadOnly => !IsWritable(); - public event SyncDictionaryChanged Callback; public enum Operation : byte { @@ -30,7 +49,7 @@ struct Change // list of changes. // -> insert/delete/clear is only ONE change - // -> changing the same slot 10x caues 10 changes. + // -> changing the same slot 10x causes 10 changes. // -> note that this grows until next sync(!) // TODO Dictionary to avoid ever growing changes / redundant changes! readonly List changes = new List(); @@ -41,13 +60,6 @@ struct Change // so we need to skip them int changesAhead; - public override void Reset() - { - changes.Clear(); - changesAhead = 0; - objects.Clear(); - } - public ICollection Keys => objects.Keys; public ICollection Values => objects.Values; @@ -56,38 +68,6 @@ public override void Reset() IEnumerable IReadOnlyDictionary.Values => objects.Values; - // throw away all the changes - // this should be called after a successful sync - public override void ClearChanges() => changes.Clear(); - - public SyncIDictionary(IDictionary objects) - { - this.objects = objects; - } - - void AddOperation(Operation op, TKey key, TValue item, bool checkAccess) - { - if (checkAccess && IsReadOnly) - { - throw new System.InvalidOperationException("SyncDictionaries can only be modified by the owner."); - } - - Change change = new Change - { - operation = op, - key = key, - item = item - }; - - if (IsRecording()) - { - changes.Add(change); - OnDirty?.Invoke(); - } - - Callback?.Invoke(op, key, item); - } - public override void OnSerializeAll(NetworkWriter writer) { // if init, write the full list content @@ -179,15 +159,15 @@ public override void OnDeserializeDelta(NetworkReader reader) // ClientToServer needs to set dirty in server OnDeserialize. // no access check: server OnDeserialize can always // write, even for ClientToServer (for broadcasting). - if (ContainsKey(key)) + if (objects.TryGetValue(key, out TValue oldItem)) { - objects[key] = item; // assign after ContainsKey check - AddOperation(Operation.OP_SET, key, item, false); + objects[key] = item; // assign after TryGetValue + AddOperation(Operation.OP_SET, key, item, oldItem, false); } else { - objects[key] = item; // assign after ContainsKey check - AddOperation(Operation.OP_ADD, key, item, false); + objects[key] = item; // assign after TryGetValue + AddOperation(Operation.OP_ADD, key, item, default, false); } } break; @@ -195,12 +175,14 @@ public override void OnDeserializeDelta(NetworkReader reader) case Operation.OP_CLEAR: 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); + AddOperation(Operation.OP_CLEAR, default, default, default, false); + // clear after invoking the callback so users can iterate the dictionary + // and take appropriate action on the items before they are wiped. + objects.Clear(); } break; @@ -208,14 +190,14 @@ public override void OnDeserializeDelta(NetworkReader reader) key = reader.Read(); if (apply) { - if (objects.TryGetValue(key, out item)) + if (objects.TryGetValue(key, out TValue oldItem)) { // add dirty + changes. // ClientToServer needs to set dirty in server OnDeserialize. // no access check: server OnDeserialize can always // write, even for ClientToServer (for broadcasting). objects.Remove(key); - AddOperation(Operation.OP_REMOVE, key, item, false); + AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false); } } break; @@ -229,22 +211,15 @@ public override void OnDeserializeDelta(NetworkReader reader) } } - public void Clear() + // throw away all the changes + // this should be called after a successful sync + public override void ClearChanges() => changes.Clear(); + + public override void Reset() { + changes.Clear(); + changesAhead = 0; objects.Clear(); - AddOperation(Operation.OP_CLEAR, default, default, true); - } - - public bool ContainsKey(TKey key) => objects.ContainsKey(key); - - public bool Remove(TKey key) - { - if (objects.TryGetValue(key, out TValue item) && objects.Remove(key)) - { - AddOperation(Operation.OP_REMOVE, key, item, true); - return true; - } - return false; } public TValue this[TKey i] @@ -254,42 +229,31 @@ public TValue this[TKey i] { if (ContainsKey(i)) { + TValue oldItem = objects[i]; objects[i] = value; - AddOperation(Operation.OP_SET, i, value, true); + AddOperation(Operation.OP_SET, i, value, oldItem, true); } else { objects[i] = value; - AddOperation(Operation.OP_ADD, i, value, true); + AddOperation(Operation.OP_ADD, i, value, default, true); } } } public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value); - public void Add(TKey key, TValue value) - { - objects.Add(key, value); - AddOperation(Operation.OP_ADD, key, value, true); - } + public bool ContainsKey(TKey key) => objects.ContainsKey(key); - public void Add(KeyValuePair item) => Add(item.Key, item.Value); - - public bool Contains(KeyValuePair item) - { - return TryGetValue(item.Key, out TValue val) && EqualityComparer.Default.Equals(val, item.Value); - } + public bool Contains(KeyValuePair item) => TryGetValue(item.Key, out TValue val) && EqualityComparer.Default.Equals(val, item.Value); public void CopyTo(KeyValuePair[] array, int arrayIndex) { if (arrayIndex < 0 || arrayIndex > array.Length) - { throw new System.ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range"); - } + if (array.Length - arrayIndex < Count) - { throw new System.ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array"); - } int i = arrayIndex; foreach (KeyValuePair item in objects) @@ -299,16 +263,80 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) } } + public void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public void Add(TKey key, TValue value) + { + objects.Add(key, value); + AddOperation(Operation.OP_ADD, key, value, default, true); + } + + public bool Remove(TKey key) + { + if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key)) + { + AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true); + return true; + } + return false; + } + public bool Remove(KeyValuePair item) { bool result = objects.Remove(item.Key); if (result) - { - AddOperation(Operation.OP_REMOVE, item.Key, item.Value, true); - } + AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true); + return result; } + public void Clear() + { + AddOperation(Operation.OP_CLEAR, default, default, default, true); + // clear after invoking the callback so users can iterate the dictionary + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + + void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess) + { + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("SyncDictionaries can only be modified by the owner."); + + Change change = new Change + { + operation = op, + key = key, + item = item + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(key); + break; + case Operation.OP_SET: + OnSet?.Invoke(key, oldItem); + break; + case Operation.OP_REMOVE: + OnRemove?.Invoke(key, oldItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + break; + } + +#pragma warning disable CS0618 // Type or member is obsolete + Callback?.Invoke(op, key, item); +#pragma warning restore CS0618 // Type or member is obsolete + } + public IEnumerator> GetEnumerator() => objects.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator(); @@ -316,9 +344,9 @@ public bool Remove(KeyValuePair item) public class SyncDictionary : SyncIDictionary { - public SyncDictionary() : base(new Dictionary()) {} - public SyncDictionary(IEqualityComparer eq) : base(new Dictionary(eq)) {} - public SyncDictionary(IDictionary d) : base(new Dictionary(d)) {} + public SyncDictionary() : base(new Dictionary()) { } + public SyncDictionary(IEqualityComparer eq) : base(new Dictionary(eq)) { } + public SyncDictionary(IDictionary d) : base(new Dictionary(d)) { } public new Dictionary.ValueCollection Values => ((Dictionary)objects).Values; public new Dictionary.KeyCollection Keys => ((Dictionary)objects).Keys; public new Dictionary.Enumerator GetEnumerator() => ((Dictionary)objects).GetEnumerator(); diff --git a/Assets/Mirror/Core/SyncList.cs b/Assets/Mirror/Core/SyncList.cs index 8b70eed22..20775ad91 100644 --- a/Assets/Mirror/Core/SyncList.cs +++ b/Assets/Mirror/Core/SyncList.cs @@ -6,23 +6,39 @@ namespace Mirror { public class SyncList : SyncObject, IList, IReadOnlyList { - public delegate void SyncListChanged(Operation op, int itemIndex, T oldItem, T newItem); + public enum Operation : byte + { + OP_ADD, + OP_SET, + OP_INSERT, + OP_REMOVEAT, + OP_CLEAR + } + + /// This is called after the item is added with index + public Action OnAdd; + + /// This is called after the item is inserted with inedx + public Action OnInsert; + + /// This is called after the item is set with index and OLD Value + public Action OnSet; + + /// This is called after the item is removed with index and OLD Value + public Action OnRemove; + + /// This is called before the list is cleared so the list can be iterated + public Action OnClear; + + // Deprecated 2024-03-23 + [Obsolete("Use individual Actions, which pass OLD values where appropriate, instead.")] + public Action Callback; readonly IList objects; readonly IEqualityComparer comparer; public int Count => objects.Count; public bool IsReadOnly => !IsWritable(); - public event SyncListChanged Callback; - - public enum Operation : byte - { - OP_ADD, - OP_CLEAR, - OP_INSERT, - OP_REMOVEAT, - OP_SET - } struct Change { @@ -43,7 +59,7 @@ struct Change // so we need to skip them int changesAhead; - public SyncList() : this(EqualityComparer.Default) {} + public SyncList() : this(EqualityComparer.Default) { } public SyncList(IEqualityComparer comparer) { @@ -71,9 +87,7 @@ public override void Reset() void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkAccess) { if (checkAccess && IsReadOnly) - { throw new InvalidOperationException("Synclists can only be modified by the owner."); - } Change change = new Change { @@ -88,7 +102,28 @@ void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkA OnDirty?.Invoke(); } + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(itemIndex); + break; + case Operation.OP_INSERT: + OnInsert?.Invoke(itemIndex); + break; + case Operation.OP_SET: + OnSet?.Invoke(itemIndex, oldItem); + break; + case Operation.OP_REMOVEAT: + OnRemove?.Invoke(itemIndex, oldItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + break; + } + +#pragma warning disable CS0618 // Type or member is obsolete Callback?.Invoke(op, itemIndex, oldItem, newItem); +#pragma warning restore CS0618 // Type or member is obsolete } public override void OnSerializeAll(NetworkWriter writer) @@ -195,12 +230,14 @@ public override void OnDeserializeDelta(NetworkReader reader) case Operation.OP_CLEAR: 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); + // clear after invoking the callback so users can iterate the list + // and take appropriate action on the items before they are wiped. + objects.Clear(); } break; @@ -265,15 +302,15 @@ public void Add(T item) public void AddRange(IEnumerable range) { foreach (T entry in range) - { Add(entry); - } } public void Clear() { - objects.Clear(); AddOperation(Operation.OP_CLEAR, 0, default, default, true); + // clear after invoking the callback so users can iterate the list + // and take appropriate action on the items before they are wiped. + objects.Clear(); } public bool Contains(T item) => IndexOf(item) >= 0; @@ -331,9 +368,8 @@ public bool Remove(T item) int index = IndexOf(item); bool result = index >= 0; if (result) - { RemoveAt(index); - } + return result; } @@ -352,9 +388,7 @@ public int RemoveAll(Predicate match) toRemove.Add(objects[i]); foreach (T entry in toRemove) - { Remove(entry); - } return toRemove.Count; } @@ -393,6 +427,7 @@ public struct Enumerator : IEnumerator { readonly SyncList list; int index; + public T Current { get; private set; } public Enumerator(SyncList list) @@ -405,16 +440,15 @@ public Enumerator(SyncList list) public bool MoveNext() { if (++index >= list.Count) - { return false; - } + Current = list[index]; return true; } public void Reset() => index = -1; object IEnumerator.Current => Current; - public void Dispose() {} + public void Dispose() { } } } } diff --git a/Assets/Mirror/Core/SyncSet.cs b/Assets/Mirror/Core/SyncSet.cs index 13d4302ae..5c8fe605a 100644 --- a/Assets/Mirror/Core/SyncSet.cs +++ b/Assets/Mirror/Core/SyncSet.cs @@ -6,19 +6,29 @@ namespace Mirror { public class SyncSet : SyncObject, ISet { - public delegate void SyncSetChanged(Operation op, T item); + /// This is called after the item is added. T is the new item. + public Action OnAdd; + + /// This is called after the item is removed. T is the OLD item + public Action OnRemove; + + /// This is called BEFORE the data is cleared + public Action OnClear; + + // Deprecated 2024-03-22 + [Obsolete("Use individual Actions, which pass OLD value where appropriate, instead.")] + public Action Callback; protected readonly ISet objects; public int Count => objects.Count; public bool IsReadOnly => !IsWritable(); - public event SyncSetChanged Callback; public enum Operation : byte { OP_ADD, - OP_CLEAR, - OP_REMOVE + OP_REMOVE, + OP_CLEAR } struct Change @@ -59,9 +69,7 @@ public override void Reset() void AddOperation(Operation op, T item, bool checkAccess) { if (checkAccess && IsReadOnly) - { throw new InvalidOperationException("SyncSets can only be modified by the owner."); - } Change change = new Change { @@ -75,7 +83,22 @@ void AddOperation(Operation op, T item, bool checkAccess) OnDirty?.Invoke(); } + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(item); + break; + case Operation.OP_REMOVE: + OnRemove?.Invoke(item); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + break; + } + +#pragma warning disable CS0618 // Type or member is obsolete Callback?.Invoke(op, item); +#pragma warning restore CS0618 // Type or member is obsolete } void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, checkAccess); @@ -86,9 +109,7 @@ public override void OnSerializeAll(NetworkWriter writer) writer.WriteUInt((uint)objects.Count); foreach (T obj in objects) - { writer.Write(obj); - } // all changes have been applied already // thus the client will need to skip all the pending changes @@ -112,13 +133,11 @@ public override void OnSerializeDelta(NetworkWriter writer) case Operation.OP_ADD: writer.Write(change.item); break; - - case Operation.OP_CLEAR: - break; - case Operation.OP_REMOVE: writer.Write(change.item); break; + case Operation.OP_CLEAR: + break; } } } @@ -171,18 +190,6 @@ public override void OnDeserializeDelta(NetworkReader reader) } break; - case Operation.OP_CLEAR: - 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; - case Operation.OP_REMOVE: item = reader.Read(); if (apply) @@ -195,6 +202,20 @@ public override void OnDeserializeDelta(NetworkReader reader) AddOperation(Operation.OP_REMOVE, item, false); } break; + + case Operation.OP_CLEAR: + if (apply) + { + // 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); + // clear after invoking the callback so users can iterate the set + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + break; } if (!apply) @@ -218,15 +239,15 @@ public bool Add(T item) void ICollection.Add(T item) { if (objects.Add(item)) - { AddOperation(Operation.OP_ADD, item, true); - } } public void Clear() { - objects.Clear(); AddOperation(Operation.OP_CLEAR, true); + // clear after invoking the callback so users can iterate the set + // and take appropriate action on the items before they are wiped. + objects.Clear(); } public bool Contains(T item) => objects.Contains(item); @@ -257,17 +278,13 @@ public void ExceptWith(IEnumerable other) // remove every element in other from this foreach (T element in other) - { Remove(element); - } } public void IntersectWith(IEnumerable other) { if (other is ISet otherSet) - { IntersectWithSet(otherSet); - } else { HashSet otherAsSet = new HashSet(other); @@ -280,12 +297,8 @@ void IntersectWithSet(ISet otherSet) List elements = new List(objects); foreach (T element in elements) - { if (!otherSet.Contains(element)) - { Remove(element); - } - } } public bool IsProperSubsetOf(IEnumerable other) => objects.IsProperSubsetOf(other); @@ -304,38 +317,26 @@ void IntersectWithSet(ISet otherSet) public void SymmetricExceptWith(IEnumerable other) { if (other == this) - { Clear(); - } else - { foreach (T element in other) - { if (!Remove(element)) - { Add(element); - } - } - } } // custom implementation so we can do our own Clear/Add/Remove for delta public void UnionWith(IEnumerable other) { if (other != this) - { foreach (T element in other) - { Add(element); - } - } } } public class SyncHashSet : SyncSet { - public SyncHashSet() : this(EqualityComparer.Default) {} - public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer ?? EqualityComparer.Default)) {} + public SyncHashSet() : this(EqualityComparer.Default) { } + public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer ?? EqualityComparer.Default)) { } // allocation free enumerator public new HashSet.Enumerator GetEnumerator() => ((HashSet)objects).GetEnumerator(); @@ -343,8 +344,8 @@ public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer public class SyncSortedSet : SyncSet { - public SyncSortedSet() : this(Comparer.Default) {} - public SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer ?? Comparer.Default)) {} + public SyncSortedSet() : this(Comparer.Default) { } + public SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer ?? Comparer.Default)) { } // allocation free enumerator public new SortedSet.Enumerator GetEnumerator() => ((SortedSet)objects).GetEnumerator(); diff --git a/Assets/Mirror/Core/Threading/ThreadLog.cs b/Assets/Mirror/Core/Threading/ThreadLog.cs index e8832b200..36dca5fd9 100644 --- a/Assets/Mirror/Core/Threading/ThreadLog.cs +++ b/Assets/Mirror/Core/Threading/ThreadLog.cs @@ -87,20 +87,23 @@ static void OnLateUpdate() { switch (entry.type) { + // add [Thread#] prefix to make it super obvious where this log message comes from. + // some projects may see unexpected messages that were previously hidden, + // since Unity wouldn't log them without ThreadLog.cs. case LogType.Log: - Debug.Log($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + Debug.Log($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); break; case LogType.Warning: - Debug.LogWarning($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + Debug.LogWarning($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); break; case LogType.Error: - Debug.LogError($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); break; case LogType.Exception: - Debug.LogError($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); break; case LogType.Assert: - Debug.LogAssertion($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + Debug.LogAssertion($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); break; } } diff --git a/Assets/Mirror/Core/Tools/Extensions.cs b/Assets/Mirror/Core/Tools/Extensions.cs index 0039f571d..196be4b19 100644 --- a/Assets/Mirror/Core/Tools/Extensions.cs +++ b/Assets/Mirror/Core/Tools/Extensions.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; +using UnityEngine; namespace Mirror { @@ -98,5 +99,14 @@ public static void Clear(this ConcurrentQueue source) } } #endif + +#if !UNITY_2021_3_OR_NEWER + // Unity 2021.2 and earlier don't have transform.GetPositionAndRotation which we use for performance in some places + public static void GetPositionAndRotation(this Transform transform, out Vector3 position, out Quaternion rotation) + { + position = transform.position; + rotation = transform.rotation; + } +#endif } } diff --git a/Assets/Mirror/Core/Transport.cs b/Assets/Mirror/Core/Transport.cs index 7e0716d48..7f488ebf3 100644 --- a/Assets/Mirror/Core/Transport.cs +++ b/Assets/Mirror/Core/Transport.cs @@ -36,6 +36,12 @@ public abstract class Transport : MonoBehaviour /// Is this transport available in the current platform? public abstract bool Available(); + /// Is this transported encrypted for secure communication? + public virtual bool IsEncrypted => false; + + /// If encrypted, which cipher is used? + public virtual string EncryptionCipher => ""; + // client ////////////////////////////////////////////////////////////// /// Called by Transport when the client connected to the server. public Action OnClientConnected; diff --git a/Assets/Mirror/Editor/LagCompensatorInspector.cs b/Assets/Mirror/Editor/LagCompensatorInspector.cs new file mode 100644 index 000000000..f706384a8 --- /dev/null +++ b/Assets/Mirror/Editor/LagCompensatorInspector.cs @@ -0,0 +1,14 @@ +using UnityEditor; + +namespace Mirror +{ + [CustomEditor(typeof(LagCompensator))] + public class LagCompensatorInspector : Editor + { + public override void OnInspectorGUI() + { + EditorGUILayout.HelpBox("Preview Component - Feedback appreciated on GitHub or Discord!", MessageType.Warning); + DrawDefaultInspector(); + } + } +} diff --git a/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta b/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta new file mode 100644 index 000000000..0f3e8f7f1 --- /dev/null +++ b/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 703e39b5385ae2e479987ff4ec0707a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef b/Assets/Mirror/Editor/Mirror.Editor.asmdef index 651b8fe02..800e67b04 100644 --- a/Assets/Mirror/Editor/Mirror.Editor.asmdef +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "", "references": [ "GUID:30817c1a0e6d646d99c048fc403f5979", + "GUID:72872094b21c16e48b631b2224833d49", "GUID:1d0b9d21c3ff546a4aa32399dfd33474" ], "includePlatforms": [ diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs b/Assets/Mirror/Editor/NetworkInformationPreview.cs index 882483679..0e1e6368f 100644 --- a/Assets/Mirror/Editor/NetworkInformationPreview.cs +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs @@ -128,7 +128,9 @@ float DrawNetworkIdentityInfo(NetworkIdentity identity, float initialX, float Y) Vector2 maxValueLabelSize = GetMaxNameLabelSize(infos); Rect labelRect = new Rect(initialX, Y, maxNameLabelSize.x, maxNameLabelSize.y); - Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y); + + // height needs a +1 to line up nicely + Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y + 1); foreach (NetworkIdentityInfo info in infos) { diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs index 2b6b325b6..0823234e7 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs @@ -98,7 +98,13 @@ public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters p // let's make it obvious why we returned null for easier debugging. // NOTE: if this fails for "System.Private.CoreLib": // ILPostProcessorReflectionImporter fixes it! - Log.Warning($"ILPostProcessorAssemblyResolver.Resolve: Failed to find file for {name}"); + + // the fix for #2503 started showing this warning for Bee.BeeDriver on mac, + // which is for compilation. we can ignore that one. + if (!name.Name.StartsWith("Bee.BeeDriver")) + { + Log.Warning($"ILPostProcessorAssemblyResolver.Resolve: Failed to find file for {name}"); + } return null; } diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs index dc48d8c28..47f1b9404 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -504,7 +504,7 @@ void GenerateSerialization(ref bool WeavingFailed) worker.Emit(OpCodes.Ldarg_1); // base worker.Emit(OpCodes.Ldarg_0); - worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference); MethodReference writeUint64Func = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); worker.Emit(OpCodes.Call, writeUint64Func); @@ -524,7 +524,7 @@ void GenerateSerialization(ref bool WeavingFailed) // Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL) // base worker.Emit(OpCodes.Ldarg_0); - worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference); // 8 bytes = long worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit); worker.Emit(OpCodes.And); diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs index 280240c1a..58a19d893 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs @@ -1,4 +1,5 @@ // finds all readers and writers and register them +using System.Collections.Generic; using System.Linq; using Mono.CecilX; using Mono.CecilX.Cil; @@ -17,6 +18,21 @@ public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver // otherwise Unity crashes when running tests ProcessMirrorAssemblyClasses(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed); + // process dependencies first, this way weaver can process types of other assemblies properly. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2503 + // + // find NetworkReader/Writer extensions in referenced assemblies + // save a copy of the collection enumerator since it appears to be modified at some point during iteration + IEnumerable assemblyReferences = CurrentAssembly.MainModule.AssemblyReferences.ToList(); + foreach (AssemblyNameReference assemblyNameReference in assemblyReferences) + { + AssemblyDefinition referencedAssembly = resolver.Resolve(assemblyNameReference); + if (referencedAssembly != null) + { + ProcessAssemblyClasses(CurrentAssembly, referencedAssembly, writers, readers, ref WeavingFailed); + } + } + // find readers/writers in the assembly we are in right now. return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed); } diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs b/Assets/Mirror/Editor/Weaver/Resolvers.cs index a9d551bcf..0af32caad 100644 --- a/Assets/Mirror/Editor/Weaver/Resolvers.cs +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs @@ -42,6 +42,38 @@ public static MethodReference ResolveMethod(TypeReference t, AssemblyDefinition return null; } + public static FieldReference ResolveField(TypeReference tr, AssemblyDefinition assembly, Logger Log, string name, ref bool WeavingFailed) + { + if (tr == null) + { + Log.Error($"Cannot resolve Field {name} without a class"); + WeavingFailed = true; + return null; + } + FieldReference field = ResolveField(tr, assembly, Log, m => m.Name == name, ref WeavingFailed); + if (field == null) + { + Log.Error($"Field not found with name {name} in type {tr.Name}", tr); + WeavingFailed = true; + } + return field; + } + + public static FieldReference ResolveField(TypeReference t, AssemblyDefinition assembly, Logger Log, System.Func predicate, ref bool WeavingFailed) + { + foreach (FieldDefinition fieldRef in t.Resolve().Fields) + { + if (predicate(fieldRef)) + { + return assembly.MainModule.ImportReference(fieldRef); + } + } + + Log.Error($"Field not found in type {t.Name}", t); + WeavingFailed = true; + return null; + } + public static MethodReference TryResolveMethodInParents(TypeReference tr, AssemblyDefinition assembly, string name) { if (tr == null) diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs index 5a988a1b2..aa0d42d1d 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs @@ -10,7 +10,7 @@ public class WeaverTypes { public MethodReference ScriptableObjectCreateInstanceMethod; - public MethodReference NetworkBehaviourDirtyBitsReference; + public FieldReference NetworkBehaviourDirtyBitsReference; public MethodReference GetWriterReference; public MethodReference ReturnWriterReference; @@ -90,7 +90,7 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail TypeReference NetworkBehaviourType = Import(); - NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, assembly, "syncVarDirtyBits"); + NetworkBehaviourDirtyBitsReference = Resolvers.ResolveField(NetworkBehaviourType, assembly, Log, "syncVarDirtyBits", ref WeavingFailed); generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed); generatedSyncVarSetter_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_GameObject", ref WeavingFailed); diff --git a/Assets/Mirror/Examples/BenchmarkPrediction.meta b/Assets/Mirror/Examples/BenchmarkPrediction.meta new file mode 100644 index 000000000..99e65128f --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7e90270b475f740d69548d4ed4ef5f7a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat b/Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat new file mode 100644 index 000000000..7506ca95f --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat @@ -0,0 +1,80 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: BallMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 1 + - _GlossyReflections: 1 + - _Metallic: 1 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat.meta b/Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat.meta new file mode 100644 index 000000000..f62a9242f --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 09fe33013804145e8a4ba1d18f834dcf +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/MirrorPredictionBenchmark.unity b/Assets/Mirror/Examples/BenchmarkPrediction/MirrorPredictionBenchmark.unity new file mode 100644 index 000000000..284ae0063 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/MirrorPredictionBenchmark.unity @@ -0,0 +1,1031 @@ +%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: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 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.44657844, g: 0.49641222, b: 0.57481676, 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: 1 + 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: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + 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: 0} +--- !u!1 &3512376 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3512380} + - component: {fileID: 3512379} + - component: {fileID: 3512378} + - component: {fileID: 3512377} + m_Layer: 0 + m_Name: SideWall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &3512377 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3512376} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &3512378 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3512376} + 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: 5afe569b0e1434398b94cf6c73e90c89, 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: 1 + 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!33 &3512379 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3512376} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &3512380 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3512376} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: -5, y: 2.5, z: 0} + m_LocalScale: {x: 10, y: 5, z: 0.1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 8 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &11554784 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 11554788} + - component: {fileID: 11554787} + - component: {fileID: 11554786} + - component: {fileID: 11554785} + m_Layer: 0 + m_Name: SideWall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &11554785 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 11554784} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &11554786 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 11554784} + 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: 5afe569b0e1434398b94cf6c73e90c89, 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: 1 + 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!33 &11554787 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 11554784} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &11554788 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 11554784} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: 5, y: 2.5, z: 0} + m_LocalScale: {x: 10, y: 5, z: 0.1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &191084098 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 191084102} + - component: {fileID: 191084101} + - component: {fileID: 191084100} + - component: {fileID: 191084099} + m_Layer: 0 + m_Name: TopWall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &191084099 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 191084098} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &191084100 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 191084098} + 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: 5afe569b0e1434398b94cf6c73e90c89, 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: 1 + 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!33 &191084101 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 191084098} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &191084102 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 191084098} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 5, z: 0} + m_LocalScale: {x: 10, y: 0.1, z: 10} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &333653952 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 333653956} + - component: {fileID: 333653955} + - component: {fileID: 333653954} + - component: {fileID: 333653953} + m_Layer: 0 + m_Name: SideWall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &333653953 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 333653952} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &333653954 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 333653952} + 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: 5afe569b0e1434398b94cf6c73e90c89, 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: 1 + 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!33 &333653955 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 333653952} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &333653956 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 333653952} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 2.5, z: -5} + m_LocalScale: {x: 10, y: 5, z: 0.1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &703590129 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 703590131} + - component: {fileID: 703590130} + 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 &703590130 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 703590129} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 0 + 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 &703590131 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 703590129} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, 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} +--- !u!1 &1192486254 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1192486258} + - component: {fileID: 1192486257} + - component: {fileID: 1192486256} + - component: {fileID: 1192486255} + m_Layer: 0 + m_Name: SideWall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &1192486255 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1192486254} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1192486256 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1192486254} + 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: 5afe569b0e1434398b94cf6c73e90c89, 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: 1 + 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!33 &1192486257 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1192486254} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1192486258 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1192486254} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 2.5, z: 5} + m_LocalScale: {x: 10, y: 5, z: 0.1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1343329356 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1343329360} + - component: {fileID: 1343329359} + - component: {fileID: 1343329358} + - component: {fileID: 1343329357} + m_Layer: 0 + m_Name: BottomWall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &1343329357 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1343329358 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + 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: 5afe569b0e1434398b94cf6c73e90c89, 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: 1 + 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!33 &1343329359 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1343329360 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 10, y: 0.1, z: 10} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1432777610 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1432777613} + - component: {fileID: 1432777612} + - component: {fileID: 1432777611} + - component: {fileID: 1432777614} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1432777611 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + 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 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 297433 + UnreliableMaxMessageSize: 1194 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!114 &1432777612 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f96c236d30fd94a75a172a7642242637, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 1 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 60 + autoStartServerBuild: 0 + autoConnectClientBuild: 0 + offlineScene: + onlineScene: + transport: {fileID: 1432777611} + networkAddress: localhost + maxConnections: 100 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 6080703956733773953, guid: feea51e51b4564f06a38482bbebac8fa, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 0 + spawnPrefabs: + - {fileID: 5646305152014201295, guid: 881505c283e224c4fbe4e03127f08b4a, type: 3} + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 0 + spawnAmount: 1000 + spawnPrefab: {fileID: 5646305152014201295, guid: 881505c283e224c4fbe4e03127f08b4a, + type: 3} + spawnArea: + m_Center: {x: 0, y: 2.5, z: 0} + m_Extent: {x: 5, y: 2.5, z: 5} +--- !u!4 &1432777613 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + 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 &1432777614 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!1 &2101508988 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2101508991} + - component: {fileID: 2101508990} + - component: {fileID: 2101508989} + - component: {fileID: 2101508992} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &2101508989 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_Enabled: 1 +--- !u!20 &2101508990 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} + 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: 5 + 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 &2101508991 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_LocalRotation: {x: -0.016399357, y: 0.9638876, z: -0.06110458, w: -0.25868532} + m_LocalPosition: {x: 7.708386, y: 3.4294498, z: 13.220224} + 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 &2101508992 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6635375fbc6be456ea640b75add6378e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + showLog: 0 diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/MirrorPredictionBenchmark.unity.meta b/Assets/Mirror/Examples/BenchmarkPrediction/MirrorPredictionBenchmark.unity.meta new file mode 100644 index 000000000..d2ec86435 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/MirrorPredictionBenchmark.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 26e96d86a94c2451d85dcabf4aff3551 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/NetworkManagerPredictionBenchmark.cs b/Assets/Mirror/Examples/BenchmarkPrediction/NetworkManagerPredictionBenchmark.cs new file mode 100644 index 000000000..7c995fd28 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/NetworkManagerPredictionBenchmark.cs @@ -0,0 +1,49 @@ +using UnityEngine; + +namespace Mirror.Examples.PredictionBenchmark +{ + public class NetworkManagerPredictionBenchmark : NetworkManager + { + [Header("Spawns")] + public int spawnAmount = 1000; + public GameObject spawnPrefab; + public Bounds spawnArea = new Bounds(new Vector3(0, 2.5f, 0), new Vector3(10f, 5f, 10f)); + + public override void Awake() + { + base.Awake(); + + // ensure vsync is disabled for the benchmark, otherwise results are capped + QualitySettings.vSyncCount = 0; + } + + void SpawnAll() + { + // spawn randomly inside the cage + for (int i = 0; i < spawnAmount; ++i) + { + // choose a random point within the cage + float x = Random.Range(spawnArea.min.x, spawnArea.max.x); + float y = Random.Range(spawnArea.min.y, spawnArea.max.y); + float z = Random.Range(spawnArea.min.z, spawnArea.max.z); + Vector3 position = new Vector3(x, y, z); + + // spawn & position + GameObject go = Instantiate(spawnPrefab); + go.transform.position = position; + NetworkServer.Spawn(go); + } + } + + public override void OnStartServer() + { + base.OnStartServer(); + SpawnAll(); + + // disable rendering on server to reduce noise in profiling. + // keep enabled in host mode though. + if (mode == NetworkManagerMode.ServerOnly) + Camera.main.enabled = false; + } + } +} diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/NetworkManagerPredictionBenchmark.cs.meta b/Assets/Mirror/Examples/BenchmarkPrediction/NetworkManagerPredictionBenchmark.cs.meta new file mode 100644 index 000000000..96adf3cc1 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/NetworkManagerPredictionBenchmark.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f96c236d30fd94a75a172a7642242637 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/PlayerSpectator.prefab b/Assets/Mirror/Examples/BenchmarkPrediction/PlayerSpectator.prefab new file mode 100644 index 000000000..089f18b2e --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/PlayerSpectator.prefab @@ -0,0 +1,51 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6080703956733773953 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5774152995658786670} + - component: {fileID: 4958697633604052194} + m_Layer: 0 + m_Name: PlayerSpectator + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5774152995658786670 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6080703956733773953} + 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!114 &4958697633604052194 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6080703956733773953} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 0 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/PlayerSpectator.prefab.meta b/Assets/Mirror/Examples/BenchmarkPrediction/PlayerSpectator.prefab.meta new file mode 100644 index 000000000..5cddbc4e8 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/PlayerSpectator.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: feea51e51b4564f06a38482bbebac8fa +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/PredictedBall.prefab b/Assets/Mirror/Examples/BenchmarkPrediction/PredictedBall.prefab new file mode 100644 index 000000000..448e00d2b --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/PredictedBall.prefab @@ -0,0 +1,190 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &5646305152014201295 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5646305152014201299} + - component: {fileID: 5646305152014201298} + - component: {fileID: 5646305152014201297} + - component: {fileID: 5646305152014201296} + - component: {fileID: 1898357413811911178} + - component: {fileID: 7187875016326091757} + - component: {fileID: 1900383403885999746} + - component: {fileID: 813163234907249251} + m_Layer: 0 + m_Name: PredictedBall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5646305152014201299 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 2.5, z: 0} + m_LocalScale: {x: 0.35, y: 0.35, z: 0.35} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &5646305152014201298 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &5646305152014201297 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + 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: 09fe33013804145e8a4ba1d18f834dcf, 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: 1 + 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!135 &5646305152014201296 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Radius: 0.5 + m_Center: {x: 0, y: 0, z: 0} +--- !u!54 &1898357413811911178 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + serializedVersion: 2 + m_Mass: 1 + m_Drag: 0 + m_AngularDrag: 0.05 + m_UseGravity: 1 + m_IsKinematic: 0 + m_Interpolate: 1 + m_Constraints: 0 + m_CollisionDetection: 1 +--- !u!114 &7187875016326091757 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 3619328764 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &1900383403885999746 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d38927cdc6024b9682b5fe9778b9ef99, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + predictedRigidbody: {fileID: 0} + mode: 1 + motionSmoothingVelocityThreshold: 0.1 + motionSmoothingAngularVelocityThreshold: 0.1 + motionSmoothingTimeTolerance: 0.5 + stateHistoryLimit: 32 + recordInterval: 0.05 + onlyRecordChanges: 1 + compareLastFirst: 1 + positionCorrectionThreshold: 0.1 + rotationCorrectionThreshold: 5 + oneFrameAhead: 1 + snapThreshold: 2 + showGhost: 0 + ghostVelocityThreshold: 0.1 + localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} + remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, type: 2} + checkGhostsEveryNthFrame: 4 + positionInterpolationSpeed: 15 + rotationInterpolationSpeed: 10 + teleportDistanceMultiplier: 10 + reduceSendsWhileIdle: 1 +--- !u!114 &813163234907249251 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5646305152014201295} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87a6103a0a29544ba9f303c8a3b7407c, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + force: 10 + interval: 3 diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/PredictedBall.prefab.meta b/Assets/Mirror/Examples/BenchmarkPrediction/PredictedBall.prefab.meta new file mode 100644 index 000000000..ae2fd846d --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/PredictedBall.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 881505c283e224c4fbe4e03127f08b4a +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/RandomForce.cs b/Assets/Mirror/Examples/BenchmarkPrediction/RandomForce.cs new file mode 100644 index 000000000..5a0cbe719 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/RandomForce.cs @@ -0,0 +1,52 @@ +using UnityEngine; + +namespace Mirror.Examples.PredictionBenchmark +{ + public class RandomForce : NetworkBehaviour + { + public float force = 10; + public float interval = 3; + PredictedRigidbody prediction; + Rigidbody rb => prediction.predictedRigidbody; + + void Awake() + { + prediction = GetComponent(); + } + + // every(!) connected client adds force to all objects(!) + // the more clients, the more crazier it gets. + // this is intentional for benchmarks. + public override void OnStartClient() + { + // start at a random time, but repeat at a fixed time + float randomStart = Random.Range(0, interval); + InvokeRepeating(nameof(ApplyForce), randomStart, interval); + } + + + [ClientCallback] + void ApplyForce() + { + // calculate force in random direction but always upwards + Vector2 direction2D = Random.insideUnitCircle; + Vector3 direction3D = new Vector3(direction2D.x, 1.0f, direction2D.y); + Vector3 impulse = direction3D * force; + + // grab the current Rigidbody from PredictedRigidbody. + // sometimes this is on a ghost object, so always grab it live: + + + // predicted locally and sync to server for others to see. + // PredictedRigidbody will take care of corrections automatically. + rb.AddForce(impulse, ForceMode.Impulse); + CmdApplyForce(impulse); + } + + [Command(requiresAuthority = false)] // everyone can call this + void CmdApplyForce(Vector3 impulse) + { + rb.AddForce(impulse, ForceMode.Impulse); + } + } +} diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/RandomForce.cs.meta b/Assets/Mirror/Examples/BenchmarkPrediction/RandomForce.cs.meta new file mode 100644 index 000000000..046bd6366 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/RandomForce.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87a6103a0a29544ba9f303c8a3b7407c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md b/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md new file mode 100644 index 000000000..f92777351 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md @@ -0,0 +1,24 @@ +Mirror's PredictedRigidbody is optimized for low end devices / VR. +While not interacting with the object, there's zero overhead! +While interacting, overhead comes from sync & corrections. + +This benchmark has predicted objects which are constantly synced & corrected. +=> This is not a real world scenario, it's worst case that we can use for profiling! +=> As a Mirror user you don't need to worry about this demo. + +# Benchmark Setup +- Unity 2021.3 LTS +- IL2CPP Builds +- M1 Macbook Pro +- vsync disabled in NetworkManagerPredictionBenchmark.cs + +# Benchmark Results History for 1000 objects without ghosts: +Not Predicted: 1000 FPS Client, 2500 FPS Server +Predicted: + 2024-03-13: 500 FPS Client, 1700 FPS Server + 2024-03-13: 580 FPS Client, 1700 FPS Server // micro optimizations + 2024-03-14: 590 FPS Client, 1700 FPS Server // UpdateGhosting() every 4th frame + 2024-03-14: 615 FPS Client, 1700 FPS Server // predictedRigidbodyTransform.GetPositionAndRotation() + 2024-03-15: 625 FPS Client, 1700 FPS Server // Vector3.MoveTowardsCustom() + 2024-03-18: 628 FPS Client, 1700 FPS Server // removed O(N) insertion from CorrectHistory() + 2024-03-28: 800 FPS Client, 1700 FPS Server // FAST mode prediction diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md.meta b/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md.meta new file mode 100644 index 000000000..b1647bc7d --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ef1cc472cf2141baa667b35be391340a +timeCreated: 1710305999 \ No newline at end of file diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/WallMaterial.mat b/Assets/Mirror/Examples/BenchmarkPrediction/WallMaterial.mat new file mode 100644 index 000000000..62efe50d4 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/WallMaterial.mat @@ -0,0 +1,82 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: WallMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ValidKeywords: + - _ALPHAPREMULTIPLY_ON + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 3000 + stringTagMap: + RenderType: Transparent + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 10 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 3 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 0} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/WallMaterial.mat.meta b/Assets/Mirror/Examples/BenchmarkPrediction/WallMaterial.mat.meta new file mode 100644 index 000000000..a68af7a37 --- /dev/null +++ b/Assets/Mirror/Examples/BenchmarkPrediction/WallMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5afe569b0e1434398b94cf6c73e90c89 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/Billiards/MirrorBilliards.unity b/Assets/Mirror/Examples/Billiards/MirrorBilliards.unity index 3b68584dd..5d80f008b 100644 --- a/Assets/Mirror/Examples/Billiards/MirrorBilliards.unity +++ b/Assets/Mirror/Examples/Billiards/MirrorBilliards.unity @@ -213,7 +213,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: height: 150 + offsetY: 40 maxLogCount: 50 + showInEditor: 0 hotKey: 293 --- !u!1001 &250045978 PrefabInstance: @@ -746,6 +748,7 @@ GameObject: - component: {fileID: 1282001519} - component: {fileID: 1282001521} - component: {fileID: 1282001522} + - component: {fileID: 1282001523} m_Layer: 0 m_Name: NetworkManager m_TagString: Untagged @@ -796,12 +799,14 @@ MonoBehaviour: m_EditorClassIdentifier: dontDestroyOnLoad: 1 runInBackground: 1 - autoStartServerBuild: 1 - autoConnectClientBuild: 0 + headlessStartMode: 1 + editorAutoStart: 0 sendRate: 120 + autoStartServerBuild: 0 + autoConnectClientBuild: 0 offlineScene: onlineScene: - transport: {fileID: 1282001521} + transport: {fileID: 1282001523} networkAddress: localhost maxConnections: 2 disconnectInactiveConnections: 0 @@ -814,6 +819,7 @@ MonoBehaviour: spawnPrefabs: - {fileID: 3429911415116987808, guid: d07e00a439ecd46e79554ec89f65317b, type: 3} - {fileID: 3429911415116987808, guid: 0100f0c90700741b496ccbc2fe54c196, type: 3} + exceptionsDisconnect: 1 snapshotSettings: bufferTimeMultiplier: 2 bufferLimit: 32 @@ -825,7 +831,8 @@ MonoBehaviour: dynamicAdjustment: 1 dynamicAdjustmentTolerance: 1 deliveryTimeEmaDuration: 2 - connectionQualityInterval: 3 + evaluationMethod: 0 + evaluationInterval: 3 timeInterpolationGui: 1 --- !u!114 &1282001521 MonoBehaviour: @@ -852,7 +859,7 @@ MonoBehaviour: MaxRetransmit: 40 MaximizeSocketBuffers: 1 ReliableMaxMessageSize: 297433 - UnreliableMaxMessageSize: 1195 + UnreliableMaxMessageSize: 1194 debugLog: 0 statisticsGUI: 0 statisticsLog: 0 @@ -872,6 +879,24 @@ MonoBehaviour: padding: 2 width: 180 height: 25 +--- !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: 96b149f511061407fb54895c057b7736, type: 3} + m_Name: + m_EditorClassIdentifier: + wrap: {fileID: 1282001521} + latency: 50 + jitter: 0.02 + jitterSpeed: 1 + unreliableLoss: 2 + unreliableScramble: 2 --- !u!1001 &1633978772 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/Mirror/Examples/Billiards/Table/Billiard Table.prefab b/Assets/Mirror/Examples/Billiards/Table/Billiard Table.prefab index 246694dad..27162224b 100644 --- a/Assets/Mirror/Examples/Billiards/Table/Billiard Table.prefab +++ b/Assets/Mirror/Examples/Billiards/Table/Billiard Table.prefab @@ -18,6 +18,7 @@ GameObject: - component: {fileID: 3539222710066621734} - component: {fileID: 3539222710066621732} - component: {fileID: 3539222710066621733} + - component: {fileID: 4431155707644151673} m_Layer: 0 m_Name: PocketsCollider m_TagString: Untagged @@ -188,6 +189,18 @@ CapsuleCollider: m_Height: 1 m_Direction: 2 m_Center: {x: -0.47, y: 0, z: 0} +--- !u!114 &4431155707644151673 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3539222710066621743} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f17c923d118b941fb90a834d87e9ff27, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1 &3539222711229660005 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Mirror/Examples/Billiards/_Readme.txt b/Assets/Mirror/Examples/Billiards/_Readme.txt index 332eb2c44..8f4c380c1 100644 --- a/Assets/Mirror/Examples/Billiards/_Readme.txt +++ b/Assets/Mirror/Examples/Billiards/_Readme.txt @@ -4,11 +4,15 @@ Mouse drag the white ball to apply force. Billiards is surprisingly easy to implement, which makes this a great demo for beginners! Hits are sent to the server with a [Command]. -There will always be some latency for the results to show. +Server simulates physics and sends results back to the client. -To solve this, there's another BilliardsPredicted demo which uses prediction & reconciliation. -This demo however is meant for complete beginners to learn Mirror! +While simple, this approach has a major flaw: latency. +The NetworkManager has a LatencySimulation component to see this on your own computer. +Client actions will always feel a bit delayed while waiting for the server. + +The solution to this is called Prediction: +https://mirror-networking.gitbook.io/docs/manual/general/client-side-prediction Notes: - Red/White ball Rigidbody CollisionMode needs to be ContinousDynamic to avoid white flying through red sometimes. - even 'Continous' is not enough, we need ContinousDynamic. \ No newline at end of file + even 'Continuous' is not enough, we need ContinuousDynamic. \ No newline at end of file diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/Pockets.cs b/Assets/Mirror/Examples/BilliardsPredicted/Ball/Pockets.cs new file mode 100644 index 000000000..2e8c27dd6 --- /dev/null +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/Pockets.cs @@ -0,0 +1,38 @@ +// script to handle the table's pocket collisions for resets / destruction. +// predicted objects sometimes have their rigidbodies moved out of them. +// which is why we handle collisions in the table itself, not per-object. +// because here we can check who the rigidbody belongs to more easily. +// ... that's just the best practice at the moment, maybe we can make this +// easier in the future ... +using UnityEngine; + +namespace Mirror.Examples.BilliardsPredicted +{ + public class Pockets : MonoBehaviour + { + void OnTriggerEnter(Collider other) + { + if (!NetworkServer.active) return; + + // the collider may be on a predicted object or on its ghost object. + // find the source first. + if (PredictedRigidbody.IsPredicted(other, out PredictedRigidbody predicted)) + { + // is it a white ball? + if (predicted.TryGetComponent(out WhiteBallPredicted white)) + { + Rigidbody rigidBody = predicted.predictedRigidbody; + rigidBody.position = white.startPosition; + rigidBody.velocity = Vector3.zero; + } + + // is it a read ball? + if (predicted.GetComponent()) + { + // destroy when entering a pocket. + NetworkServer.Destroy(predicted.gameObject); + } + } + } + } +} diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/Pockets.cs.meta b/Assets/Mirror/Examples/BilliardsPredicted/Ball/Pockets.cs.meta new file mode 100644 index 000000000..4908954b9 --- /dev/null +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/Pockets.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f17c923d118b941fb90a834d87e9ff27 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedBallPredicted.cs b/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedBallPredicted.cs index 8797dbeec..352e385ee 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedBallPredicted.cs +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedBallPredicted.cs @@ -1,9 +1,15 @@ + using UnityEngine; namespace Mirror.Examples.BilliardsPredicted { + // keep the empty script so we can find out what type of ball we collided with. public class RedBallPredicted : NetworkBehaviour { + /* ball<->pocket collisions are handled by Pockets.cs for now. + because predicted object's rigidbodies are sometimes moved out of them. + which means this script here wouldn't get the collision info while predicting. + which means it's easier to check collisions from the table perspective. // destroy when entering a pocket. // there's only one trigger in the scene (the pocket). [ServerCallback] @@ -11,5 +17,6 @@ void OnTriggerEnter(Collider other) { NetworkServer.Destroy(gameObject); } + */ } } diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab b/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab index dcb3c1941..daa3591f7 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab @@ -100,11 +100,10 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} m_Name: m_EditorClassIdentifier: - clientStarted: 0 sceneId: 0 _assetId: 776221176 serverOnly: 0 - visible: 0 + visibility: 0 hasSpawned: 0 --- !u!135 &3429911415116987811 SphereCollider: @@ -164,10 +163,26 @@ MonoBehaviour: m_EditorClassIdentifier: syncDirection: 0 syncMode: 0 - syncInterval: 0.1 + syncInterval: 0 + predictedRigidbody: {fileID: -177125271246800426} + mode: 1 + motionSmoothingVelocityThreshold: 0.1 + motionSmoothingAngularVelocityThreshold: 5 + motionSmoothingTimeTolerance: 0.5 stateHistoryLimit: 32 - correctionThreshold: 0.1 + recordInterval: 0.05 + onlyRecordChanges: 1 + compareLastFirst: 1 + positionCorrectionThreshold: 0.1 + rotationCorrectionThreshold: 5 oneFrameAhead: 1 - correctionMode: 1 - ghostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} - lineTime: 10 + snapThreshold: 2 + showGhost: 1 + ghostVelocityThreshold: 0.1 + localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} + remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, type: 2} + checkGhostsEveryNthFrame: 4 + positionInterpolationSpeed: 15 + rotationInterpolationSpeed: 10 + teleportDistanceMultiplier: 10 + reduceSendsWhileIdle: 1 diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhiteBallPredicted.cs b/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhiteBallPredicted.cs index 038650867..4178ea421 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhiteBallPredicted.cs +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhiteBallPredicted.cs @@ -1,3 +1,4 @@ +using System; using UnityEngine; namespace Mirror.Examples.BilliardsPredicted @@ -5,12 +6,15 @@ namespace Mirror.Examples.BilliardsPredicted public class WhiteBallPredicted : NetworkBehaviour { public LineRenderer dragIndicator; + public float dragTolerance = 1.0f; public Rigidbody rigidBody; public float forceMultiplier = 2; public float maxForce = 40; // remember start position to reset to after entering a pocket - Vector3 startPosition; + internal Vector3 startPosition; + + bool draggingStartedOverObject; // cast mouse position on screen to world position bool MouseToWorld(out Vector3 position) @@ -31,6 +35,77 @@ void Awake() startPosition = transform.position; } + [ClientCallback] + void Update() + { + // mouse down on the white ball? + if (Input.GetMouseButtonDown(0)) + { + if (MouseToWorld(out Vector3 position)) + { + // allow dragging if mouse is 'close enough'. + // balls are moving so we don't need to be exactly on it. + float distance = Vector3.Distance(position, transform.position); + if (distance <= dragTolerance) + { + // enable drag indicator + dragIndicator.SetPosition(0, transform.position); + dragIndicator.SetPosition(1, transform.position); + dragIndicator.gameObject.SetActive(true); + + draggingStartedOverObject = true; + } + } + } + // mouse button dragging? + else if (Input.GetMouseButton(0)) + { + // cast mouse position to world + if (draggingStartedOverObject && MouseToWorld(out Vector3 current)) + { + // drag indicator + dragIndicator.SetPosition(0, transform.position); + dragIndicator.SetPosition(1, current); + } + } + // mouse button up? + else if (Input.GetMouseButtonUp(0)) + { + // cast mouse position to world + if (draggingStartedOverObject && MouseToWorld(out Vector3 current)) + { + // calculate delta from ball to mouse + // ball may have moved since we started dragging, + // so always use current ball position here. + Vector3 from = transform.position; + + // debug drawing: only works if Gizmos are enabled! + Debug.DrawLine(from, current, Color.white, 2); + + // calculate pending force delta + Vector3 delta = from - current; + Vector3 force = delta * forceMultiplier; + + // there should be a maximum allowed force + force = Vector3.ClampMagnitude(force, maxForce); + + // forward the event to the local player's object. + // the ball isn't part of the local player. + NetworkClient.localPlayer.GetComponent().OnDraggedBall(force); + + // disable drag indicator + dragIndicator.gameObject.SetActive(false); + } + + draggingStartedOverObject = false; + } + } + + // OnMouse callbacks don't work for predicted objects because we need to + // move the collider out of the main object ocassionally. + // besides, having a drag tolerance and not having to click exactly on + // the white ball is nice. + /* [ClientCallback] void OnMouseDown() { @@ -79,7 +154,12 @@ void OnMouseUp() // disable drag indicator dragIndicator.gameObject.SetActive(false); } + */ + /* ball<->pocket collisions are handled by Pockets.cs for now. + because predicted object's rigidbodies are sometimes moved out of them. + which means this script here wouldn't get the collision info while predicting. + which means it's easier to check collisions from the table perspective. // reset position when entering a pocket. // there's only one trigger in the scene (the pocket). [ServerCallback] @@ -89,6 +169,7 @@ void OnTriggerEnter(Collider other) rigidBody.Sleep(); // reset forces // GetComponent().RpcTeleport(startPosition); } + */ [ClientCallback] void OnGUI() diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab b/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab index e9dd1d0d3..d1b4d76d0 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab @@ -232,11 +232,10 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} m_Name: m_EditorClassIdentifier: - clientStarted: 0 sceneId: 0 _assetId: 2140274599 serverOnly: 0 - visible: 0 + visibility: 0 hasSpawned: 0 --- !u!135 &3429911415116987811 SphereCollider: @@ -283,6 +282,7 @@ MonoBehaviour: syncMode: 0 syncInterval: 0 dragIndicator: {fileID: 982362982} + dragTolerance: 1 rigidBody: {fileID: 1848203816128897140} forceMultiplier: 2 maxForce: 40 @@ -300,10 +300,26 @@ MonoBehaviour: m_EditorClassIdentifier: syncDirection: 0 syncMode: 0 - syncInterval: 0.1 + syncInterval: 0 + predictedRigidbody: {fileID: 1848203816128897140} + mode: 1 + motionSmoothingVelocityThreshold: 0.1 + motionSmoothingAngularVelocityThreshold: 5 + motionSmoothingTimeTolerance: 0.5 stateHistoryLimit: 32 - correctionThreshold: 0.1 + recordInterval: 0.05 + onlyRecordChanges: 1 + compareLastFirst: 1 + positionCorrectionThreshold: 0.1 + rotationCorrectionThreshold: 5 oneFrameAhead: 1 - correctionMode: 1 - ghostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} - lineTime: 10 + snapThreshold: 2 + showGhost: 1 + ghostVelocityThreshold: 0.1 + localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} + remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, type: 2} + checkGhostsEveryNthFrame: 4 + positionInterpolationSpeed: 15 + rotationInterpolationSpeed: 10 + teleportDistanceMultiplier: 10 + reduceSendsWhileIdle: 1 diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs b/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs index 95f43bfc7..19dbc488f 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs +++ b/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs @@ -23,14 +23,10 @@ public class PlayerPredicted : NetworkBehaviour // white ball component WhiteBallPredicted whiteBall; - // keep a history of inputs with timestamp - public int inputHistorySize = 64; - readonly SortedList inputs = new SortedList(); - void Awake() { // find the white ball once -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER whiteBall = FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 @@ -45,29 +41,29 @@ void ApplyForceToWhite(Vector3 force) // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Rigidbody.AddForce.html // this is buffered until the next FixedUpdate. + // get the white ball's Rigidbody. + // prediction sometimes moves this out of the object for a while, + // so we need to grab it this way: + Rigidbody rb = whiteBall.GetComponent().predictedRigidbody; + // AddForce has different force modes, see this excellent diagram: // https://www.reddit.com/r/Unity3D/comments/psukm1/know_the_difference_between_forcemodes_a_little/ // for prediction it's extremely important(!) to apply the correct mode: // 'Force' makes server & client drift significantly here // 'Impulse' is correct usage with significantly less drift - whiteBall.GetComponent().AddForce(force, ForceMode.Impulse); + rb.AddForce(force, ForceMode.Impulse); } // called when the local player dragged the white ball. // we reuse the white ball's OnMouseDrag and forward the event to here. public void OnDraggedBall(Vector3 force) { - // record the input for reconciliation if needed - if (inputs.Count >= inputHistorySize) inputs.RemoveAt(0); - inputs.Add(NetworkTime.time, new PlayerInput(NetworkTime.time, force)); - Debug.Log($"Inputs.Count={inputs.Count}"); - // apply force locally immediately ApplyForceToWhite(force); // apply on server as well. // not necessary in host mode, otherwise we would apply it twice. - if (!isServer) CmdApplyForce(force, NetworkTime.predictedTime); + if (!isServer) CmdApplyForce(force); } // while prediction is applied on clients immediately, @@ -80,7 +76,7 @@ public void OnDraggedBall(Vector3 force) // TODO send over unreliable with ack, notify, etc. later [Command] - void CmdApplyForce(Vector3 force, double predictedTime) + void CmdApplyForce(Vector3 force) { if (!IsValidMove(force)) { @@ -88,30 +84,6 @@ void CmdApplyForce(Vector3 force, double predictedTime) return; } - // client is on a predicted timeline. - // double check the prediction - it should arrive at server time. - // - // there are multiple reasons why this may be off: - // - time prediction may still be adjusting itself - // - time prediction may have an issue - // - server or client may be lagging or under heavy load temporarily - // - unreliable vs. reliable channel latencies are signifcantly different - // for example, if latency simulation is only applied to one channel! - double delta = NetworkTime.time - predictedTime; - if (delta < -0.010) - { - Debug.LogWarning($"Cmd predictedTime was {(delta*1000):F0}ms behind the server time. This could occasionally happen if the time prediction is off. If it happens consistently, check that unreliable NetworkTime and reliable [Command]s have the same latency. If they are off, this will cause heavy jitter."); - } - else if (delta > 0.010) - { - // TODO consider buffering inputs which are ahead, apply next frame - Debug.LogWarning($"Cmd predictedTime was {(delta*1000):F0}ms ahead of the server time. This could occasionally happen if the time prediction is off. If it happens consistently, check that unreliable NetworkTime and reliable [Command]s have the same latency. If they are off, this will cause heavy jitter. If reliable & unreliable latency are similar and this still happens a lot, consider buffering inputs for the next frame."); - } - else - { - Debug.Log($"Cmd predictedTime was {(delta*1000):F0}ms close to the server time."); - } - // apply force ApplyForceToWhite(force); } diff --git a/Assets/Mirror/Examples/BilliardsPredicted/_Readme.txt b/Assets/Mirror/Examples/BilliardsPredicted/_Readme.txt index 578e9a1b5..4e0a04cca 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/_Readme.txt +++ b/Assets/Mirror/Examples/BilliardsPredicted/_Readme.txt @@ -1,12 +1,18 @@ Advanced multiplayer Billiards demo with Prediction. + +Please read this first: +https://mirror-networking.gitbook.io/docs/manual/general/client-side-prediction + Mouse drag the white ball to apply force. PredictedRigidbody syncInterval is intentionally set pretty high so we can see when it corrects. If you are a beginner, start with the basic Billiards demo instead. If you are advanced, this demo shows how to use Mirror's prediction features for physics / FPS games. -The demo is work in progress. -At the moment, this is only for the Mirror team to test individual prediction features! +Billiards is a great example to try our Prediction algorithm, it works extremely well here! + +=> We use 'Fast' Prediction mode for Billiards because we want to see exact collisions with balls/walls. +=> 'Smooth' mode would look too soft, with balls changing direction even before touching other balls/walls. Notes: - Red/White ball Rigidbody CollisionMode needs to be ContinousDynamic to avoid white flying through red sometimes. diff --git a/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerControllerScript.cs b/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerControllerScript.cs index 7504df6cd..24cf59dc5 100644 --- a/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerControllerScript.cs +++ b/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerControllerScript.cs @@ -83,7 +83,7 @@ public override void OnStartAuthority() characterController.enabled = true; this.enabled = true; -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER sceneReferencer = GameObject.FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 diff --git a/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerEmpty.cs b/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerEmpty.cs index 71ffa492b..e44fa8a10 100644 --- a/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerEmpty.cs +++ b/Assets/Mirror/Examples/CharacterSelection/Scripts/PlayerEmpty.cs @@ -10,7 +10,7 @@ public class PlayerEmpty : NetworkBehaviour public override void OnStartAuthority() { // enable UI located in the scene, after empty player spawns in. -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER sceneReferencer = GameObject.FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 diff --git a/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayer.cs b/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayer.cs index ce7026995..f50a8f1cf 100644 --- a/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayer.cs +++ b/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayer.cs @@ -26,7 +26,7 @@ public override void OnStartAuthority() if (isOwned) { -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER couchPlayerManager = GameObject.FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 diff --git a/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayerManager.cs b/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayerManager.cs index 19172f4f2..d65204811 100644 --- a/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayerManager.cs +++ b/Assets/Mirror/Examples/CouchCoop/Scripts/CouchPlayerManager.cs @@ -23,7 +23,7 @@ public class CouchPlayerManager : NetworkBehaviour public override void OnStartAuthority() { // hook up UI to local player, for cmd communication -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER canvasScript = GameObject.FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 diff --git a/Assets/Mirror/Examples/EdgegapLobby.meta b/Assets/Mirror/Examples/EdgegapLobby.meta new file mode 100644 index 000000000..2a4478f67 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8befc60066f3f148ab1ab4120064045 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/EdgegapLobbyTanks.unity b/Assets/Mirror/Examples/EdgegapLobby/EdgegapLobbyTanks.unity new file mode 100644 index 000000000..11443176c --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/EdgegapLobbyTanks.unity @@ -0,0 +1,1104 @@ +%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: 4890085278179872738, guid: 1cb229a9b0b434acf9cb6b263057a2a0, + type: 2} +--- !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: 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!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: 1 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} + 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: 5 + 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, y: 0.92387956, z: -0.38268343, w: 0} + m_LocalPosition: {x: 0, y: 6.5, z: 8} + 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: 180, 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: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 150 + offsetY: 40 + maxLogCount: 50 + showInEditor: 0 + hotKey: 293 +--- !u!1 &251893064 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 251893065} + - component: {fileID: 251893066} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &251893065 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 251893064} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 3, y: 0, z: 3} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &251893066 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 251893064} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !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: Spawn + 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: 3, y: 0, z: -3} + 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!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 &1018416663 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1018416666} + - component: {fileID: 1018416665} + - component: {fileID: 1018416664} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1018416664 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1018416663} + 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 &1018416665 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1018416663} + 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 &1018416666 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1018416663} + 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: 9 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1107091652 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1107091656} + - component: {fileID: 1107091655} + - component: {fileID: 1107091654} + - component: {fileID: 1107091653} + m_Layer: 0 + m_Name: Ground + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 4294967295 + m_IsActive: 1 +--- !u!23 &1107091653 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + 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: 4294967295 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 29b49c27a74f145918356859bd7af511, 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: 1 + 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!64 &1107091654 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + 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 &1107091655 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1107091656 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + 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: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !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: 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: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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: 8aab4c8111b7c411b9b92cf3dbc5bd4e, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 1 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 120 + autoStartServerBuild: 0 + autoConnectClientBuild: 0 + offlineScene: + onlineScene: + transport: {fileID: 1282001523} + networkAddress: localhost + maxConnections: 100 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 1916082411674582, guid: 6f43bf5488a7443d19ab2a83c6b91f35, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 1 + spawnPrefabs: + - {fileID: 5890560936853567077, guid: b7dd46dbf38c643f09e206f9fa4be008, type: 3} + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 1 +--- !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: bc654f29862fc2643b948f772ebb9e68, type: 3} + m_Name: + m_EditorClassIdentifier: + color: {r: 1, g: 1, b: 1, a: 1} + padding: 2 + width: 180 + height: 25 +--- !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: fa9d4c3f48a245ed89f122f44e1e81ea, type: 3} + m_Name: + m_EditorClassIdentifier: + port: 7777 + DualMode: 1 + NoDelay: 1 + Interval: 10 + Timeout: 10000 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 294131 + UnreliableMaxMessageSize: 1181 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 + relayAddress: 127.0.0.1 + relayGameServerPort: 8888 + relayGameClientPort: 9999 + relayGUI: 0 + userId: 11111111 + sessionId: 22222222 + lobbyUrl: + lobbyWaitTimeout: 60 +--- !u!1 &1458789072 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1458789073} + - component: {fileID: 1458789074} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1458789073 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1458789072} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -3, y: 0, z: 3} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1458789074 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1458789072} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1501912662 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1501912663} + - component: {fileID: 1501912664} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1501912663 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1501912662} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -3, y: 0, z: -3} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1501912664 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1501912662} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !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: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.802082 + 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.10938167, y: 0.8754261, z: -0.40821788, w: 0.23456976} + 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: 2 + m_LocalEulerAnglesHint: {x: 50, y: 150, z: 0} +--- !u!1001 &1813424392215010339 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1813424391128259723, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391128259723, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391128259723, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391128259723, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391128259723, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391128259723, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391319593378, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391319593378, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391319593378, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391319593378, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391319593378, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391336397939, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391612798255, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391612798255, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391612798255, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391612798255, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391612798255, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 108.06613 + objectReference: {fileID: 0} + - target: {fileID: 1813424391612798255, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391872098077, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391872098077, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391872098077, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391872098077, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391872098077, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424391872098077, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_RootOrder + value: 8 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932097, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392052932157, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_Name + value: LobbyUI + objectReference: {fileID: 0} + - target: {fileID: 1813424392113017367, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392113017367, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392113017367, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392118041961, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392118041961, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392118041961, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392118041961, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392118041961, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392250510888, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392250510888, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392250510888, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392250510888, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392250510888, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392369884078, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1813424392369884078, guid: ebc1436948da70b4abbf74f58106c318, + type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: ebc1436948da70b4abbf74f58106c318, type: 3} diff --git a/Assets/Mirror/Examples/EdgegapLobby/EdgegapLobbyTanks.unity.meta b/Assets/Mirror/Examples/EdgegapLobby/EdgegapLobbyTanks.unity.meta new file mode 100644 index 000000000..7265d3996 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/EdgegapLobbyTanks.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5dbbfee253d4c6e4d915cb88674ec680 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/Prefabs.meta b/Assets/Mirror/Examples/EdgegapLobby/Prefabs.meta new file mode 100644 index 000000000..1caf106e5 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 776ba2248d912ad4b839e39448ad4a9c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUI.prefab b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUI.prefab new file mode 100644 index 000000000..e26515c12 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUI.prefab @@ -0,0 +1,4738 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1813424390310845841 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390310845840} + - component: {fileID: 1813424390310845846} + - component: {fileID: 1813424390310845847} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390310845840 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390310845841} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.0000001, y: 1.0000001, z: 1.0000001} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1813424391989063747} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424390310845846 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390310845841} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390310845847 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390310845841} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Slots +--- !u!1 &1813424390339664491 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390339664490} + - component: {fileID: 1813424390339664495} + - component: {fileID: 1813424390339664488} + - component: {fileID: 1813424390339664494} + m_Layer: 5 + m_Name: Create + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &1813424390339664490 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390339664491} + 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: 1813424390449590486} + - {fileID: 1813424391979909271} + - {fileID: 1813424390443808431} + - {fileID: 1813424391252743550} + - {fileID: 1813424391576538892} + m_Father: {fileID: 1813424392052932097} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.3} + m_AnchorMax: {x: 1, y: 0.7} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -800, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424390339664495 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390339664491} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390339664488 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390339664491} + 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: 0.392} + 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: 10907, 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!114 &1813424390339664494 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390339664491} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6d48c41753254160ac6a02c9585880f0, type: 3} + m_Name: + m_EditorClassIdentifier: + List: {fileID: 1813424391429320923} + CancelButton: {fileID: 1813424391979909270} + LobbyName: {fileID: 1813424392160392958} + SlotCount: {fileID: 1813424392203108450} + SlotSlider: {fileID: 1813424391401318742} + HostButton: {fileID: 1813424391872098076} + ServerButton: {fileID: 1813424391128259722} +--- !u!1 &1813424390365182142 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390365182141} + - component: {fileID: 1813424390365182082} + - component: {fileID: 1813424390365182083} + - component: {fileID: 1813424390365182140} + m_Layer: 5 + m_Name: Scrollbar Horizontal + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390365182141 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390365182142} + 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: 1813424390512863189} + m_Father: {fileID: 1813424392182487710} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 20} + m_Pivot: {x: 0, y: 0} +--- !u!222 &1813424390365182082 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390365182142} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390365182083 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390365182142} + 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: 10907, 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!114 &1813424390365182140 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390365182142} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, 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: 1813424392113017366} + m_HandleRect: {fileID: 1813424392113017367} + m_Direction: 0 + m_Value: 1 + m_Size: 0.99999994 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424390398780282 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390398780281} + m_Layer: 5 + m_Name: Lobby + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390398780281 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390398780282} + 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: 1813424391907589263} + - {fileID: 1813424392160392959} + m_Father: {fileID: 1813424391252743550} + 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: 712.61523, y: -25} + m_SizeDelta: {x: 1425.2305, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1813424390443808424 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390443808431} + - component: {fileID: 1813424390443808429} + - component: {fileID: 1813424390443808430} + m_Layer: 5 + m_Name: Sep (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390443808431 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390443808424} + 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: 1813424390339664490} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -80} + m_SizeDelta: {x: 0, y: 2} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424390443808429 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390443808424} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390443808430 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390443808424} + 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: 0.1792453, g: 0.1792453, b: 0.1792453, a: 0.6156863} + 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: 0} + m_Type: 0 + 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!1 &1813424390449590487 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390449590486} + - component: {fileID: 1813424390449590484} + - component: {fileID: 1813424390449590485} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390449590486 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390449590487} + 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: 1813424390339664490} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -15.00006} + m_SizeDelta: {x: 0, y: 50} + m_Pivot: {x: 0.5, y: 1} +--- !u!222 &1813424390449590484 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390449590487} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390449590485 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390449590487} + 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: 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_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 46 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 4 + m_MaxSize: 46 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Create Lobby +--- !u!1 &1813424390492288766 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390492288765} + - component: {fileID: 1813424390492288707} + - component: {fileID: 1813424390492288764} + m_Layer: 5 + m_Name: Error + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390492288765 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390492288766} + 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: 1813424391429320918} + m_RootOrder: 6 + 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!222 &1813424390492288707 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390492288766} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390492288764 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390492288766} + 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: 0.6320754, g: 0.10435206, b: 0.10435206, a: 1} + m_RaycastTarget: 0 + 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: 50 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 5 + m_MaxSize: 50 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!1 &1813424390504895425 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390504895424} + m_Layer: 5 + m_Name: Fill Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390504895424 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390504895425} + 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: 1813424391769037174} + m_Father: {fileID: 1813424391401318743} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: -5, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1813424390512863190 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390512863189} + m_Layer: 5 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390512863189 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390512863190} + 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: 1813424392113017367} + m_Father: {fileID: 1813424390365182141} + 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: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1813424390567381427 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390567381426} + - component: {fileID: 1813424390567381431} + - component: {fileID: 1813424390567381424} + - component: {fileID: 1813424390567381425} + m_Layer: 5 + m_Name: Search + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390567381426 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390567381427} + 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: 1813424392333929481} + - {fileID: 1813424391964014149} + m_Father: {fileID: 1813424391429320918} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 150, y: -15} + m_SizeDelta: {x: 300, y: 50} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424390567381431 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390567381427} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390567381424 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390567381427} + 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: 10911, 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!114 &1813424390567381425 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390567381427} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, 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: 1813424390567381424} + m_TextComponent: {fileID: 1813424391964014148} + m_Placeholder: {fileID: 1813424392333929480} + m_ContentType: 0 + m_InputType: 0 + m_AsteriskChar: 42 + m_KeyboardType: 0 + m_LineType: 0 + m_HideMobileInput: 0 + m_CharacterValidation: 0 + m_CharacterLimit: 0 + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnDidEndEdit: + m_PersistentCalls: + m_Calls: [] + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] + 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} + m_Text: + m_CaretBlinkRate: 0.85 + m_CaretWidth: 1 + m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 +--- !u!1 &1813424390584036411 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390584036410} + - component: {fileID: 1813424390584036408} + - component: {fileID: 1813424390584036409} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390584036410 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390584036411} + 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: 1813424392118041961} + 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!222 &1813424390584036408 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390584036411} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390584036409 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390584036411} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Disband lobby +--- !u!1 &1813424390598854041 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390598854040} + - component: {fileID: 1813424390598854046} + - component: {fileID: 1813424390598854047} + m_Layer: 5 + m_Name: Loading + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390598854040 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390598854041} + 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: 1813424391429320918} + m_RootOrder: 5 + 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!222 &1813424390598854046 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390598854041} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390598854047 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390598854041} + 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: 0.7830189, g: 0.7830189, b: 0.7830189, a: 1} + m_RaycastTarget: 0 + 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: 50 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 5 + m_MaxSize: 50 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Loading... +--- !u!1 &1813424390737110186 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424390737110185} + - component: {fileID: 1813424390737110191} + - component: {fileID: 1813424390737110184} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424390737110185 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390737110186} + 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: 1813424391128259723} + 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!222 &1813424390737110191 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390737110186} + m_CullTransparentMesh: 1 +--- !u!114 &1813424390737110184 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424390737110186} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Server Only +--- !u!1 &1813424391128259716 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391128259723} + - component: {fileID: 1813424391128259720} + - component: {fileID: 1813424391128259721} + - component: {fileID: 1813424391128259722} + m_Layer: 5 + m_Name: ServerButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391128259723 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391128259716} + 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: 1813424390737110185} + m_Father: {fileID: 1813424391576538892} + 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: 214.53458, y: -25} + m_SizeDelta: {x: 429.06915, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391128259720 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391128259716} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391128259721 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391128259716} + 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!114 &1813424391128259722 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391128259716} + 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: 1813424391128259721} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391201774642 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391201774641} + - component: {fileID: 1813424391201774647} + - component: {fileID: 1813424391201774640} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391201774641 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391201774642} + 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: 1813424392299290341} + 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!222 &1813424391201774647 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391201774642} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391201774640 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391201774642} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Refresh +--- !u!1 &1813424391252743551 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391252743550} + - component: {fileID: 1813424391252743548} + - component: {fileID: 1813424391252743549} + m_Layer: 5 + m_Name: Content + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391252743550 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391252743551} + 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: 1813424390398780281} + - {fileID: 1813424391989063747} + m_Father: {fileID: 1813424390339664490} + m_RootOrder: 3 + 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: -30, y: -200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1813424391252743548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391252743551} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 15 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 1 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!222 &1813424391252743549 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391252743551} + m_CullTransparentMesh: 1 +--- !u!1 &1813424391319593379 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391319593378} + - component: {fileID: 1813424391319593383} + - component: {fileID: 1813424391319593376} + - component: {fileID: 1813424391319593377} + m_Layer: 5 + m_Name: StopServer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391319593378 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391319593379} + 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: 1813424391963965835} + m_Father: {fileID: 1813424391336397939} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 200, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424391319593383 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391319593379} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391319593376 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391319593379} + 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!114 &1813424391319593377 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391319593379} + 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: 1813424391319593376} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391336397932 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391336397939} + - component: {fileID: 1813424391336397943} + - component: {fileID: 1813424391336397936} + - component: {fileID: 1813424391336397937} + - component: {fileID: 1813424391336397938} + - component: {fileID: 1813424391336397942} + m_Layer: 5 + m_Name: Status + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391336397939 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391336397932} + 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: 1813424391612798255} + - {fileID: 1813424392118041961} + - {fileID: 1813424391319593378} + - {fileID: 1813424392250510888} + m_Father: {fileID: 1813424392052932097} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 14.999878, y: 15} + m_SizeDelta: {x: 0, y: 70} + m_Pivot: {x: 0, y: 0} +--- !u!222 &1813424391336397943 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391336397932} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391336397936 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391336397932} + 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: 0.392} + 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: 10907, 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!114 &1813424391336397937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391336397932} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 15 + m_Right: 15 + m_Top: 15 + m_Bottom: 15 + m_ChildAlignment: 0 + m_Spacing: 15 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 0 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!114 &1813424391336397938 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391336397932} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 2 + m_VerticalFit: 0 +--- !u!114 &1813424391336397942 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391336397932} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 44d2f1170bbe4432bf6f388bcfabefee, type: 3} + m_Name: + m_EditorClassIdentifier: + ShowDisconnected: + - {fileID: 1813424391429320919} + ShowServer: + - {fileID: 1813424391319593379} + ShowHost: + - {fileID: 1813424392118041962} + ShowClient: + - {fileID: 1813424392250510889} + StopServer: {fileID: 1813424391319593377} + StopHost: {fileID: 1813424392118041960} + StopClient: {fileID: 1813424392250510895} + StatusText: {fileID: 1813424391612798254} +--- !u!1 &1813424391388274153 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391388274152} + - component: {fileID: 1813424391388274157} + - component: {fileID: 1813424391388274158} + - component: {fileID: 1813424391388274159} + m_Layer: 5 + m_Name: CreateButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391388274152 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391388274153} + 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: 1813424391715162631} + m_Father: {fileID: 1813424391429320918} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -15, y: -15} + m_SizeDelta: {x: 200, y: 50} + m_Pivot: {x: 1, y: 1} +--- !u!222 &1813424391388274157 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391388274153} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391388274158 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391388274153} + 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!114 &1813424391388274159 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391388274153} + 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: 1813424391388274158} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391401318736 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391401318743} + - component: {fileID: 1813424391401318742} + m_Layer: 5 + m_Name: Slider + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391401318743 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391401318736} + 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: 1813424392037622259} + - {fileID: 1813424390504895424} + - {fileID: 1813424391960583919} + m_Father: {fileID: 1813424391989063747} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: -42.5, y: 0} + m_SizeDelta: {x: -115, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1813424391401318742 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391401318736} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 67db9e8f0e2ae9c40bc1e2b64352a6b4, 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: 1813424392055451902} + m_FillRect: {fileID: 1813424391769037174} + m_HandleRect: {fileID: 1813424392055451903} + m_Direction: 0 + m_MinValue: 2 + m_MaxValue: 8 + m_WholeNumbers: 1 + m_Value: 5 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391429320919 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391429320918} + - component: {fileID: 1813424391429320916} + - component: {fileID: 1813424391429320917} + - component: {fileID: 1813424391429320923} + m_Layer: 5 + m_Name: List + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391429320918 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391429320919} + 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: 1813424392299290341} + - {fileID: 1813424390567381426} + - {fileID: 1813424392100063167} + - {fileID: 1813424391388274152} + - {fileID: 1813424392182487710} + - {fileID: 1813424390598854040} + - {fileID: 1813424390492288765} + m_Father: {fileID: 1813424392052932097} + m_RootOrder: 1 + 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: -500, y: -200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391429320916 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391429320919} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391429320917 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391429320919} + 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: 0.392} + 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: 10907, 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!114 &1813424391429320923 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391429320919} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c5e5ad322f314077a66f889b58485188, type: 3} + m_Name: + m_EditorClassIdentifier: + Create: {fileID: 1813424390339664494} + EntryPrefab: {fileID: 1861598604614510488, guid: 9ad36d24bb59d094dbf84bf5bbbdd1c6, + type: 3} + LobbyContent: {fileID: 1813424391464433542} + Loading: {fileID: 1813424390598854041} + RefreshButton: {fileID: 1813424392299290340} + SearchInput: {fileID: 1813424390567381425} + CreateButton: {fileID: 1813424391388274159} + Error: {fileID: 1813424390492288764} +--- !u!1 &1813424391443419228 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391443419299} + - component: {fileID: 1813424391443419296} + - component: {fileID: 1813424391443419297} + - component: {fileID: 1813424391443419298} + m_Layer: 5 + m_Name: Scrollbar Vertical + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391443419299 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391443419228} + 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: 1813424391961327392} + m_Father: {fileID: 1813424392182487710} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 0} + m_Pivot: {x: 1, y: 1} +--- !u!222 &1813424391443419296 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391443419228} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391443419297 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391443419228} + 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: 10907, 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!114 &1813424391443419298 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391443419228} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, 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: 1813424392369884077} + m_HandleRect: {fileID: 1813424392369884078} + m_Direction: 2 + m_Value: 1 + m_Size: 1 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391464433543 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391464433542} + - component: {fileID: 1813424391464433541} + m_Layer: 5 + m_Name: Content + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391464433542 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391464433543} + 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: 1813424391775670328} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.0001373291} + m_SizeDelta: {x: 0, y: 300} + m_Pivot: {x: 0, y: 1} +--- !u!114 &1813424391464433541 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391464433543} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 1 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &1813424391573466367 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391573466366} + - component: {fileID: 1813424391573466364} + - component: {fileID: 1813424391573466365} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391573466366 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391573466367} + 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: 1813424392250510888} + 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!222 &1813424391573466364 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391573466367} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391573466365 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391573466367} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Leave lobby +--- !u!1 &1813424391576538893 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391576538892} + - component: {fileID: 1813424391576538899} + m_Layer: 5 + m_Name: Buttons + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391576538892 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391576538893} + 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: 1813424391128259723} + - {fileID: 1813424391872098077} + m_Father: {fileID: 1813424390339664490} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.2, y: 0} + m_AnchorMax: {x: 0.8, y: 0} + m_AnchoredPosition: {x: 0, y: 30} + m_SizeDelta: {x: 0, y: 50} + m_Pivot: {x: 0.5, y: 0} +--- !u!114 &1813424391576538899 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391576538893} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 15 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &1813424391612798248 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391612798255} + - component: {fileID: 1813424391612798253} + - component: {fileID: 1813424391612798254} + - component: {fileID: 1813424391612798252} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391612798255 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391612798248} + 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: 1813424391336397939} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 108.06613, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391612798253 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391612798248} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391612798254 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391612798248} + 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: 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_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 32 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Disconnected +--- !u!114 &1813424391612798252 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391612798248} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 2 + m_VerticalFit: 0 +--- !u!1 &1813424391709694741 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391709694740} + - component: {fileID: 1813424391709694746} + - component: {fileID: 1813424391709694747} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391709694740 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391709694741} + 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: 1813424391979909271} + 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!222 &1813424391709694746 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391709694741} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391709694747 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391709694741} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Back +--- !u!1 &1813424391715162624 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391715162631} + - component: {fileID: 1813424391715162629} + - component: {fileID: 1813424391715162630} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391715162631 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391715162624} + 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: 1813424391388274152} + 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!222 &1813424391715162629 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391715162624} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391715162630 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391715162624} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Create Lobby +--- !u!1 &1813424391769037175 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391769037174} + - component: {fileID: 1813424391769037172} + - component: {fileID: 1813424391769037173} + m_Layer: 5 + m_Name: Fill + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391769037174 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391769037175} + 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: 1813424390504895424} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391769037172 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391769037175} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391769037173 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391769037175} + 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!1 &1813424391775670329 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391775670328} + - component: {fileID: 1813424391775670333} + - component: {fileID: 1813424391775670334} + - component: {fileID: 1813424391775670335} + m_Layer: 5 + m_Name: Viewport + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391775670328 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391775670329} + 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: 1813424391464433542} + m_Father: {fileID: 1813424392182487710} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + 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, y: 1} +--- !u!222 &1813424391775670333 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391775670329} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391775670334 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391775670329} + 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: 10917, 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!114 &1813424391775670335 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391775670329} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ShowMaskGraphic: 0 +--- !u!1 &1813424391872098078 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391872098077} + - component: {fileID: 1813424391872098146} + - component: {fileID: 1813424391872098147} + - component: {fileID: 1813424391872098076} + m_Layer: 5 + m_Name: HostButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391872098077 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391872098078} + 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: 1813424392017665531} + m_Father: {fileID: 1813424391576538892} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 658.60376, y: -25} + m_SizeDelta: {x: 429.06915, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391872098146 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391872098078} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391872098147 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391872098078} + 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!114 &1813424391872098076 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391872098078} + 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: 1813424391872098147} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391907589256 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391907589263} + - component: {fileID: 1813424391907589261} + - component: {fileID: 1813424391907589262} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391907589263 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391907589256} + 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: 1813424390398780281} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: -15, y: 0} + m_SizeDelta: {x: -30, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391907589261 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391907589256} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391907589262 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391907589256} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Lobby Name +--- !u!1 &1813424391960583912 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391960583919} + m_Layer: 5 + m_Name: Handle Slide Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391960583919 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391960583912} + 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: 1813424392055451903} + m_Father: {fileID: 1813424391401318743} + m_RootOrder: 2 + 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: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1813424391961327393 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391961327392} + m_Layer: 5 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391961327392 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391961327393} + 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: 1813424392369884078} + m_Father: {fileID: 1813424391443419299} + 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: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1813424391963965828 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391963965835} + - component: {fileID: 1813424391963965833} + - component: {fileID: 1813424391963965834} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391963965835 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391963965828} + 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: 1813424391319593378} + 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!222 &1813424391963965833 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391963965828} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391963965834 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391963965828} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Disband lobby +--- !u!1 &1813424391964014150 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391964014149} + - component: {fileID: 1813424391964014155} + - component: {fileID: 1813424391964014148} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391964014149 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391964014150} + 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: 1813424390567381426} + m_RootOrder: 1 + 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.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391964014155 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391964014150} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391964014148 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391964014150} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 0 + m_HorizontalOverflow: 1 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!1 &1813424391979909264 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391979909271} + - component: {fileID: 1813424391979909268} + - component: {fileID: 1813424391979909269} + - component: {fileID: 1813424391979909270} + m_Layer: 5 + m_Name: CancelButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391979909271 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391979909264} + 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: 1813424391709694740} + m_Father: {fileID: 1813424390339664490} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 15, y: -15} + m_SizeDelta: {x: 137.4, y: 50} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424391979909268 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391979909264} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391979909269 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391979909264} + 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!114 &1813424391979909270 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391979909264} + 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: 1813424391979909269} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424391989063804 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391989063747} + m_Layer: 5 + m_Name: Slots + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391989063747 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391989063804} + 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: 1813424390310845840} + - {fileID: 1813424391401318743} + - {fileID: 1813424392203108451} + m_Father: {fileID: 1813424391252743550} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 712.61523, y: -115} + m_SizeDelta: {x: 1425.2305, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1813424391998362324 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424391998362331} + - component: {fileID: 1813424391998362329} + - component: {fileID: 1813424391998362330} + m_Layer: 5 + m_Name: Placeholder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424391998362331 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391998362324} + 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: 1813424392160392959} + 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.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424391998362329 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391998362324} + m_CullTransparentMesh: 1 +--- !u!114 &1813424391998362330 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424391998362324} + 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: 0.65882355} + 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: 26 + m_FontStyle: 2 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Enter your lobby name +--- !u!1 &1813424392017665524 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392017665531} + - component: {fileID: 1813424392017665529} + - component: {fileID: 1813424392017665530} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392017665531 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392017665524} + 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: 1813424391872098077} + 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!222 &1813424392017665529 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392017665524} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392017665530 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392017665524} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Host +--- !u!1 &1813424392037622252 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392037622259} + - component: {fileID: 1813424392037622257} + - component: {fileID: 1813424392037622258} + m_Layer: 5 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392037622259 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392037622252} + 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: 1813424391401318743} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392037622257 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392037622252} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392037622258 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392037622252} + 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: 10907, 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!1 &1813424392052932157 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392052932097} + - component: {fileID: 1813424392052932098} + - component: {fileID: 1813424392052932099} + - component: {fileID: 1813424392052932156} + m_Layer: 5 + m_Name: LobbyUI + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392052932097 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392052932157} + 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: 1813424391336397939} + - {fileID: 1813424391429320918} + - {fileID: 1813424390339664490} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + 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, y: 0} +--- !u!223 &1813424392052932098 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392052932157} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!114 &1813424392052932099 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392052932157} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1920, y: 1080} + m_ScreenMatchMode: 1 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!114 &1813424392052932156 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392052932157} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!1 &1813424392055451896 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392055451903} + - component: {fileID: 1813424392055451901} + - component: {fileID: 1813424392055451902} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392055451903 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392055451896} + 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: 1813424391960583919} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 30, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392055451901 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392055451896} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392055451902 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392055451896} + 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: 10913, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 0 + 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!1 &1813424392100063160 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392100063167} + - component: {fileID: 1813424392100063165} + - component: {fileID: 1813424392100063166} + m_Layer: 5 + m_Name: Sep + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392100063167 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392100063160} + 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: 1813424391429320918} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -80} + m_SizeDelta: {x: -10, y: 2} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392100063165 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392100063160} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392100063166 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392100063160} + 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: 0.1792453, g: 0.1792453, b: 0.1792453, a: 0.6156863} + 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: 0} + m_Type: 0 + 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!1 &1813424392113017360 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392113017367} + - component: {fileID: 1813424392113017365} + - component: {fileID: 1813424392113017366} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392113017367 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392113017360} + 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: 1813424390512863189} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + 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} +--- !u!222 &1813424392113017365 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392113017360} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392113017366 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392113017360} + 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!1 &1813424392118041962 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392118041961} + - component: {fileID: 1813424392118041966} + - component: {fileID: 1813424392118041967} + - component: {fileID: 1813424392118041960} + m_Layer: 5 + m_Name: StopHost + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392118041961 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392118041962} + 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: 1813424390584036410} + m_Father: {fileID: 1813424391336397939} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 200, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424392118041966 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392118041962} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392118041967 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392118041962} + 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!114 &1813424392118041960 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392118041962} + 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: 1813424392118041967} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424392160392952 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392160392959} + - component: {fileID: 1813424392160392956} + - component: {fileID: 1813424392160392957} + - component: {fileID: 1813424392160392958} + m_Layer: 5 + m_Name: LobbyNameInput + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392160392959 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392160392952} + 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: 1813424391998362331} + - {fileID: 1813424392349128169} + m_Father: {fileID: 1813424390398780281} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -100, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424392160392956 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392160392952} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392160392957 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392160392952} + 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: 10911, 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!114 &1813424392160392958 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392160392952} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, 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: 1813424392160392957} + m_TextComponent: {fileID: 1813424392349128168} + m_Placeholder: {fileID: 1813424391998362330} + m_ContentType: 0 + m_InputType: 0 + m_AsteriskChar: 42 + m_KeyboardType: 0 + m_LineType: 0 + m_HideMobileInput: 0 + m_CharacterValidation: 0 + m_CharacterLimit: 0 + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnDidEndEdit: + m_PersistentCalls: + m_Calls: [] + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] + 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} + m_Text: + m_CaretBlinkRate: 0.85 + m_CaretWidth: 1 + m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 +--- !u!1 &1813424392182487711 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392182487710} + - component: {fileID: 1813424392182487708} + - component: {fileID: 1813424392182487709} + m_Layer: 5 + m_Name: Scroll View + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392182487710 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392182487711} + 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: 1813424391775670328} + - {fileID: 1813424390365182141} + - {fileID: 1813424391443419299} + m_Father: {fileID: 1813424391429320918} + m_RootOrder: 4 + 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: -41.5} + m_SizeDelta: {x: 0, y: -83} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392182487708 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392182487711} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392182487709 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392182487711} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Content: {fileID: 1813424391464433542} + m_Horizontal: 1 + m_Vertical: 1 + m_MovementType: 1 + m_Elasticity: 0.1 + m_Inertia: 1 + m_DecelerationRate: 0.135 + m_ScrollSensitivity: 1 + m_Viewport: {fileID: 1813424391775670328} + m_HorizontalScrollbar: {fileID: 1813424390365182140} + m_VerticalScrollbar: {fileID: 1813424391443419298} + m_HorizontalScrollbarVisibility: 2 + m_VerticalScrollbarVisibility: 2 + m_HorizontalScrollbarSpacing: -3 + m_VerticalScrollbarSpacing: -3 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424392203108380 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392203108451} + - component: {fileID: 1813424392203108449} + - component: {fileID: 1813424392203108450} + m_Layer: 5 + m_Name: SlotCounter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392203108451 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392203108380} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.0000001, y: 1.0000001, z: 1.0000001} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1813424391989063747} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -42.5, y: -5} + m_SizeDelta: {x: -115, y: 10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392203108449 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392203108380} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392203108450 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392203108380} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 36 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 7 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 5 +--- !u!1 &1813424392250510889 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392250510888} + - component: {fileID: 1813424392250510893} + - component: {fileID: 1813424392250510894} + - component: {fileID: 1813424392250510895} + m_Layer: 5 + m_Name: StopClient + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392250510888 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392250510889} + 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: 1813424391573466366} + m_Father: {fileID: 1813424391336397939} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 180, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424392250510893 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392250510889} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392250510894 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392250510889} + 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!114 &1813424392250510895 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392250510889} + 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: 1813424392250510894} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424392299290342 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392299290341} + - component: {fileID: 1813424392299290346} + - component: {fileID: 1813424392299290347} + - component: {fileID: 1813424392299290340} + m_Layer: 5 + m_Name: RefreshButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392299290341 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392299290342} + 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: 1813424391201774641} + m_Father: {fileID: 1813424391429320918} + 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: 15, y: -15} + m_SizeDelta: {x: 120, y: 50} + m_Pivot: {x: 0, y: 1} +--- !u!222 &1813424392299290346 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392299290342} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392299290347 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392299290342} + 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!114 &1813424392299290340 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392299290342} + 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: 1813424392299290347} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1813424392333929482 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392333929481} + - component: {fileID: 1813424392333929487} + - component: {fileID: 1813424392333929480} + m_Layer: 5 + m_Name: Placeholder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392333929481 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392333929482} + 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: 1813424390567381426} + 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.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392333929487 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392333929482} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392333929480 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392333929482} + 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: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} + 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: 26 + m_FontStyle: 2 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Type to search... +--- !u!1 &1813424392349128170 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392349128169} + - component: {fileID: 1813424392349128175} + - component: {fileID: 1813424392349128168} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392349128169 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392349128170} + 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: 1813424392160392959} + m_RootOrder: 1 + 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.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1813424392349128175 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392349128170} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392349128168 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392349128170} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 26 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 0 + m_HorizontalOverflow: 1 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!1 &1813424392369884079 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1813424392369884078} + - component: {fileID: 1813424392369884076} + - component: {fileID: 1813424392369884077} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1813424392369884078 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392369884079} + 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: 1813424391961327392} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + 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} +--- !u!222 &1813424392369884076 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392369884079} + m_CullTransparentMesh: 1 +--- !u!114 &1813424392369884077 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1813424392369884079} + 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 diff --git a/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUI.prefab.meta b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUI.prefab.meta new file mode 100644 index 000000000..ebb8ef26f --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUI.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ebc1436948da70b4abbf74f58106c318 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUIEntry.prefab b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUIEntry.prefab new file mode 100644 index 000000000..f0abeb3a2 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUIEntry.prefab @@ -0,0 +1,301 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1861598604008055398 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1861598604008055397} + - component: {fileID: 1861598604008055395} + - component: {fileID: 1861598604008055396} + m_Layer: 5 + m_Name: PlayerCount + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1861598604008055397 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604008055398} + 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: 1861598604614510503} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.6, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0.000030517578} + m_SizeDelta: {x: -30, y: 0} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1861598604008055395 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604008055398} + m_CullTransparentMesh: 1 +--- !u!114 &1861598604008055396 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604008055398} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 46 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 4 + m_MaxSize: 46 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 1/5 +--- !u!1 &1861598604367147692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1861598604367147691} + - component: {fileID: 1861598604367147689} + - component: {fileID: 1861598604367147690} + m_Layer: 5 + m_Name: Name + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1861598604367147691 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604367147692} + 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: 1861598604614510503} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0.6, y: 1} + m_AnchoredPosition: {x: 30, y: 0.000030517578} + m_SizeDelta: {x: -30, y: 0} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1861598604367147689 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604367147692} + m_CullTransparentMesh: 1 +--- !u!114 &1861598604367147690 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604367147692} + 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: 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: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 46 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 4 + m_MaxSize: 46 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: A long lobby name +--- !u!1 &1861598604614510488 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1861598604614510503} + - component: {fileID: 1861598604614510500} + - component: {fileID: 1861598604614510501} + - component: {fileID: 1861598604614510502} + - component: {fileID: 827505188} + m_Layer: 5 + m_Name: LobbyUIEntry + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1861598604614510503 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604614510488} + 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: 1861598604367147691} + - {fileID: 1861598604008055397} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1861598604614510500 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604614510488} + m_CullTransparentMesh: 1 +--- !u!114 &1861598604614510501 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604614510488} + 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: 0.392} + 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: 10907, 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!114 &1861598604614510502 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604614510488} + 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: 1861598604614510501} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &827505188 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1861598604614510488} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9bd3228b44c0a7e478964d95c512cebf, type: 3} + m_Name: + m_EditorClassIdentifier: + JoinButton: {fileID: 1861598604614510502} + Name: {fileID: 1861598604367147690} + PlayerCount: {fileID: 1861598604008055396} diff --git a/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUIEntry.prefab.meta b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUIEntry.prefab.meta new file mode 100644 index 000000000..7d0b7a273 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Prefabs/LobbyUIEntry.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9ad36d24bb59d094dbf84bf5bbbdd1c6 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts.meta b/Assets/Mirror/Examples/EdgegapLobby/Scripts.meta new file mode 100644 index 000000000..2355d2225 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: db78997560ea9e94fafeeca27eb3e4f0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyCreate.cs b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyCreate.cs new file mode 100644 index 000000000..8425dc05c --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyCreate.cs @@ -0,0 +1,54 @@ +using Edgegap; +using UnityEngine; +using UnityEngine.UI; + +namespace Mirror.Examples.EdgegapLobby +{ + public class UILobbyCreate : MonoBehaviour + { + public UILobbyList List; + public Button CancelButton; + public InputField LobbyName; + public Text SlotCount; + public Slider SlotSlider; + public Button HostButton; + public Button ServerButton; + private EdgegapLobbyKcpTransport _transport => (EdgegapLobbyKcpTransport)NetworkManager.singleton.transport; + + private void Awake() + { + ValidateName(); + LobbyName.onValueChanged.AddListener(_ => + { + ValidateName(); + }); + CancelButton.onClick.AddListener(() => + { + List.gameObject.SetActive(true); + gameObject.SetActive(false); + }); + SlotSlider.onValueChanged.AddListener(arg0 => + { + SlotCount.text = ((int)arg0).ToString(); + }); + HostButton.onClick.AddListener(() => + { + gameObject.SetActive(false); + _transport.SetServerLobbyParams(LobbyName.text, (int)SlotSlider.value); + NetworkManager.singleton.StartHost(); + }); + ServerButton.onClick.AddListener(() => + { + gameObject.SetActive(false); + _transport.SetServerLobbyParams(LobbyName.text, (int)SlotSlider.value); + NetworkManager.singleton.StartServer(); + }); + } + void ValidateName() + { + bool valid = !string.IsNullOrWhiteSpace(LobbyName.text); + HostButton.interactable = valid; + ServerButton.interactable = valid; + } + } +} diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyCreate.cs.meta b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyCreate.cs.meta new file mode 100644 index 000000000..6a334ce59 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyCreate.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6d48c41753254160ac6a02c9585880f0 +timeCreated: 1709967491 \ No newline at end of file diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyEntry.cs b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyEntry.cs new file mode 100644 index 000000000..235b7972d --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyEntry.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Edgegap; +using UnityEngine; +using UnityEngine.UI; + +namespace Mirror.Examples.EdgegapLobby +{ + public class UILobbyEntry : MonoBehaviour + { + public Button JoinButton; + public Text Name; + public Text PlayerCount; + + private LobbyBrief _lobby; + private UILobbyList _list; + private void Awake() + { + JoinButton.onClick.AddListener(() => + { + _list.Join(_lobby); + }); + } + + public void Init(UILobbyList list, LobbyBrief lobby, bool active = true) + { + gameObject.SetActive(active && lobby.is_joinable); + JoinButton.interactable = lobby.available_slots > 0; + _list = list; + _lobby = lobby; + Name.text = lobby.name; + PlayerCount.text = $"{lobby.player_count}/{lobby.capacity}"; + } + } + +} diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyEntry.cs.meta b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyEntry.cs.meta new file mode 100644 index 000000000..da7a25311 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9bd3228b44c0a7e478964d95c512cebf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs new file mode 100644 index 000000000..74a4a3b5e --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using Edgegap; +using UnityEngine; +using UnityEngine.UI; +namespace Mirror.Examples.EdgegapLobby +{ + public class UILobbyList : MonoBehaviour + { + public UILobbyCreate Create; + + public GameObject EntryPrefab; + public Transform LobbyContent; + public GameObject Loading; + public Button RefreshButton; + public InputField SearchInput; + public Button CreateButton; + public Text Error; + private List _entries = new List(); + + private EdgegapLobbyKcpTransport _transport => (EdgegapLobbyKcpTransport)NetworkManager.singleton.transport; + private void Awake() + { + SearchInput.onValueChanged.AddListener(arg0 => + { + SetLobbies(_transport.Api.Lobbies); + }); + RefreshButton.onClick.AddListener(Refresh); + CreateButton.onClick.AddListener(() => + { + Create.gameObject.SetActive(true); + gameObject.SetActive(false); + }); + } + public void Start() + { + Refresh(); + } + + private void Refresh() + { + Loading.SetActive(true); + _transport.Api.RefreshLobbies(SetLobbies, s => + { + Error.text = s; + Loading.SetActive(false); + }); + } + + public void Join(LobbyBrief lobby) + { + NetworkManager.singleton.networkAddress = lobby.lobby_id; + NetworkManager.singleton.StartClient(); + } + + public void SetLobbies(LobbyBrief[] lobbies) + { + Loading.SetActive(false); + Error.text = ""; + // Create enough entries + for (int i = _entries.Count; i < lobbies.Length; i++) + { + var go = Instantiate(EntryPrefab, LobbyContent); + _entries.Add(go.GetComponent()); + } + + // Update entries + var searchText = SearchInput.text; + for (int i = 0; i < lobbies.Length; i++) + { + _entries[i].Init( + this, + lobbies[i], + // search filter + searchText.Length == 0 || +#if UNITY_2021_3_OR_NEWER + lobbies[i].name.Contains(searchText, StringComparison.InvariantCultureIgnoreCase) +#else + lobbies[i].name.IndexOf(searchText, StringComparison.InvariantCultureIgnoreCase) >= 0 +#endif + ); + } + + // hide entries that are too many + for (int i = lobbies.Length; i < _entries.Count; i++) + { + _entries[i].gameObject.SetActive(false); + } + } + } +} diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs.meta b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs.meta new file mode 100644 index 000000000..4710347b6 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c5e5ad322f314077a66f889b58485188 +timeCreated: 1709962378 \ No newline at end of file diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyStatus.cs b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyStatus.cs new file mode 100644 index 000000000..756711714 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyStatus.cs @@ -0,0 +1,121 @@ +using System; +using Edgegap; +using UnityEngine; +using UnityEngine.UI; +namespace Mirror.Examples.EdgegapLobby +{ + public class UILobbyStatus : MonoBehaviour + { + public GameObject[] ShowDisconnected; + public GameObject[] ShowServer; + public GameObject[] ShowHost; + public GameObject[] ShowClient; + public Button StopServer; + public Button StopHost; + public Button StopClient; + public Text StatusText; + private Status _status; + private EdgegapLobbyKcpTransport _transport; + enum Status + { + Offline, + Server, + Host, + Client + } + void Awake() + { + Refresh(); + StopServer.onClick.AddListener(() => + { + NetworkManager.singleton.StopServer(); + }); + StopHost.onClick.AddListener(() => + { + NetworkManager.singleton.StopHost(); + }); + StopClient.onClick.AddListener(() => + { + NetworkManager.singleton.StopClient(); + }); + } + private void Start() + { + _transport = (EdgegapLobbyKcpTransport)NetworkManager.singleton.transport; + } + private void Update() + { + var status = GetStatus(); + if (_status != status) + { + _status = status; + Refresh(); + } + if (_transport) + { + StatusText.text = _transport.Status.ToString(); + } + else + { + StatusText.text = ""; + } + } + private void Refresh() + { + switch (_status) + { + + case Status.Offline: + SetUI(ShowServer, false); + SetUI(ShowHost, false); + SetUI(ShowClient, false); + SetUI(ShowDisconnected, true); + break; + case Status.Server: + SetUI(ShowDisconnected, false); + SetUI(ShowHost, false); + SetUI(ShowClient, false); + SetUI(ShowServer, true); + break; + case Status.Host: + SetUI(ShowDisconnected, false); + SetUI(ShowServer, false); + SetUI(ShowClient, false); + SetUI(ShowHost, true); + break; + case Status.Client: + SetUI(ShowDisconnected, false); + SetUI(ShowServer, false); + SetUI(ShowHost, false); + SetUI(ShowClient, true); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void SetUI(GameObject[] gos, bool active) + { + foreach (GameObject go in gos) + { + go.SetActive(active); + } + } + private Status GetStatus() + { + if (NetworkServer.active && NetworkClient.active) + { + return Status.Host; + } + if (NetworkServer.active) + { + return Status.Server; + } + if (NetworkClient.active) + { + return Status.Client; + } + return Status.Offline; + } + } +} diff --git a/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyStatus.cs.meta b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyStatus.cs.meta new file mode 100644 index 000000000..00930fb7b --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/Scripts/UILobbyStatus.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44d2f1170bbe4432bf6f388bcfabefee +timeCreated: 1710138272 \ No newline at end of file diff --git a/Assets/Mirror/Examples/EdgegapLobby/_ReadMe.txt b/Assets/Mirror/Examples/EdgegapLobby/_ReadMe.txt new file mode 100644 index 000000000..25a60e954 --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/_ReadMe.txt @@ -0,0 +1,11 @@ +Docs: https://mirror-networking.gitbook.io/docs/manual/examples/edgegap-lobby +This is a copy of the Tanks example (basic scene with player controlled tanks), +but with a lobby ui for using Edgegap's Lobby and Relay service. +It showcases how one might interact with the EdgegapLobbyKcpTransport to list, join and create lobbies. +Providing a good starting point for anyone wanting to use Edgegap lobbies. + +# Setup +As this example uses external services from Edgegap you will need to set up the transport +on the NetworkManager gameobject before you can use it. +Please see the EdgegapLobbyKcpTransport Setup instructions on how to do that: +https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-relay-transport#setup \ No newline at end of file diff --git a/Assets/Mirror/Examples/EdgegapLobby/_ReadMe.txt.meta b/Assets/Mirror/Examples/EdgegapLobby/_ReadMe.txt.meta new file mode 100644 index 000000000..97133a10c --- /dev/null +++ b/Assets/Mirror/Examples/EdgegapLobby/_ReadMe.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a6c3a72e7e659a7459a3ba3adb15b2e0 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs b/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs index be6623899..1a55c23f9 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs +++ b/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchController.cs @@ -33,7 +33,7 @@ public class MatchController : NetworkBehaviour void Awake() { -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER canvasController = GameObject.FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 @@ -58,7 +58,9 @@ IEnumerator AddPlayersToMatchController() public override void OnStartClient() { - matchPlayerData.Callback += UpdateWins; +#pragma warning disable CS0618 // Type or member is obsolete + matchPlayerData.Callback = UpdateWins; +#pragma warning restore CS0618 // Type or member is obsolete canvasGroup.alpha = 1f; canvasGroup.interactable = true; diff --git a/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchGUI.cs b/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchGUI.cs index 92f62dd92..2051efbac 100644 --- a/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchGUI.cs +++ b/Assets/Mirror/Examples/MultipleMatches/Scripts/MatchGUI.cs @@ -19,7 +19,7 @@ public class MatchGUI : MonoBehaviour public void Awake() { -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER canvasController = GameObject.FindAnyObjectByType(); #else // Deprecated in Unity 2023.1 diff --git a/Assets/Mirror/Examples/StackedPrediction.meta b/Assets/Mirror/Examples/StackedPrediction.meta new file mode 100644 index 000000000..ca839f7cb --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: de2e290d32892467c9fe2e6db83557d3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.mat b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.mat new file mode 100644 index 000000000..c8e791dc4 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.mat @@ -0,0 +1,81 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: CubeMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ValidKeywords: + - _GLOSSYREFLECTIONS_OFF + m_InvalidKeywords: [] + m_LightmapFlags: 4 + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 0 + - _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.6885808, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.mat.meta b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.mat.meta new file mode 100644 index 000000000..10a95199d --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38254cb9b1a9c497385399ae2a4a6509 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.physicMaterial b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.physicMaterial new file mode 100644 index 000000000..9756b44e2 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.physicMaterial @@ -0,0 +1,14 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!134 &13400000 +PhysicMaterial: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: CubeMaterial + dynamicFriction: 0.6 + staticFriction: 0.6 + bounciness: 0 + frictionCombine: 0 + bounceCombine: 0 diff --git a/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.physicMaterial.meta b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.physicMaterial.meta new file mode 100644 index 000000000..2b7e82799 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/CubeMaterial.physicMaterial.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7341570099774486d9de82fa640e73ab +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 13400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/GroundMaterial.mat b/Assets/Mirror/Examples/StackedPrediction/GroundMaterial.mat new file mode 100644 index 000000000..d984fedff --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/GroundMaterial.mat @@ -0,0 +1,82 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: GroundMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ValidKeywords: + - _GLOSSYREFLECTIONS_OFF + - _SPECULARHIGHLIGHTS_OFF + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 0 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 0 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0, g: 0.41486025, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Examples/StackedPrediction/GroundMaterial.mat.meta b/Assets/Mirror/Examples/StackedPrediction/GroundMaterial.mat.meta new file mode 100644 index 000000000..4af981e77 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/GroundMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63d74576506d24042aaa1b6beaf830a0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/MirrorStackedPrediction.unity b/Assets/Mirror/Examples/StackedPrediction/MirrorStackedPrediction.unity new file mode 100644 index 000000000..3d2ba38db --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/MirrorStackedPrediction.unity @@ -0,0 +1,562 @@ +%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: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 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.44657606, g: 0.4964097, b: 0.57481474, 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: 1 + 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: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + 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: 0} +--- !u!1 &703590129 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 703590131} + - component: {fileID: 703590130} + 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 &703590130 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 703590129} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 1 + 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 &703590131 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 703590129} + m_LocalRotation: {x: 0.422216, y: 0.039532606, z: -0.018434355, w: 0.9054452} + m_LocalPosition: {x: 0, y: 3, 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: 5, z: 0} +--- !u!1 &1343329356 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1343329360} + - component: {fileID: 1343329359} + - component: {fileID: 1343329358} + - component: {fileID: 1343329357} + m_Layer: 0 + m_Name: Ground + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!65 &1343329357 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1343329358 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + 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: 63d74576506d24042aaa1b6beaf830a0, 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: 1 + 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!33 &1343329359 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1343329360 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1343329356} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: -0.5, z: 0} + m_LocalScale: {x: 1000, y: 0.1, z: 1000} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1432777610 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1432777613} + - component: {fileID: 1432777612} + - component: {fileID: 1432777611} + - component: {fileID: 1432777614} + - component: {fileID: 1432777615} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1432777611 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + 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 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 297433 + UnreliableMaxMessageSize: 1194 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!114 &1432777612 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe94388660a5e45688a685723da14b57, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 1 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 60 + autoStartServerBuild: 0 + autoConnectClientBuild: 0 + offlineScene: + onlineScene: + transport: {fileID: 1432777611} + networkAddress: localhost + maxConnections: 100 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 6080703956733773953, guid: dc62ed4e37b7d4551b8c6a5edc0b36b4, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 0 + spawnPrefabs: + - {fileID: 1161087258037110271, guid: 8e024f835ef4842998861f956ca7cb5c, type: 3} + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 0 + spawnAmount: 400 + spawnPrefab: {fileID: 1161087258037110271, guid: 8e024f835ef4842998861f956ca7cb5c, + type: 3} + interleave: 1 + solverIterations: 200 +--- !u!4 &1432777613 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + 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 &1432777614 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!114 &1432777615 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1432777610} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bc654f29862fc2643b948f772ebb9e68, type: 3} + m_Name: + m_EditorClassIdentifier: + color: {r: 1, g: 1, b: 1, a: 1} + padding: 2 + width: 150 + height: 25 +--- !u!1 &2101508988 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2101508991} + - component: {fileID: 2101508990} + - component: {fileID: 2101508989} + - component: {fileID: 2101508992} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &2101508989 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_Enabled: 1 +--- !u!20 &2101508990 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} + 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: 5 + 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 &2101508991 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_LocalRotation: {x: 0.2943274, y: -0.28922528, z: 0.09395835, w: 0.9060309} + m_LocalPosition: {x: 29.62062, y: 44.436695, z: -42.575485} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 21.515, y: -149.954, z: 0} +--- !u!114 &2101508992 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2101508988} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6635375fbc6be456ea640b75add6378e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + showLog: 0 diff --git a/Assets/Mirror/Examples/StackedPrediction/MirrorStackedPrediction.unity.meta b/Assets/Mirror/Examples/StackedPrediction/MirrorStackedPrediction.unity.meta new file mode 100644 index 000000000..28f30f482 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/MirrorStackedPrediction.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d4dfee5fdad6e4e5992b5bb20418ecaa +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/NetworkManagerStackedPrediction.cs b/Assets/Mirror/Examples/StackedPrediction/NetworkManagerStackedPrediction.cs new file mode 100644 index 000000000..febe1631d --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/NetworkManagerStackedPrediction.cs @@ -0,0 +1,77 @@ +using UnityEngine; + +namespace Mirror.Examples.PredictionBenchmark +{ + public class NetworkManagerStackedPrediction : NetworkManager + { + [Header("Spawns")] + public int spawnAmount = 1000; + public GameObject spawnPrefab; + public float interleave = 1; + + // 500 objects need around 100 iterations to be stable + [Tooltip("Stacked Cubes are only stable if solver iterations are high enough!")] + public int solverIterations = 200; + + public override void Awake() + { + base.Awake(); + + // ensure vsync is disabled for the benchmark, otherwise results are capped + QualitySettings.vSyncCount = 0; + + // stacked cubes are only stable if solver iteration is high enough! + int before = Physics.defaultSolverIterations; + Physics.defaultSolverIterations = solverIterations; + Debug.Log($"Physics.defaultSolverIterations: {before} -> {Physics.defaultSolverIterations}"); + } + + void SpawnAll() + { + // 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 spawnY = 0; spawnY < sqrt; ++spawnY) + { + // spawn exactly the amount, not any more + // (our sqrt method isn't 100% precise) + if (spawned < spawnAmount) + { + // it's important to have them at least 'Physics.defaultContactOffset' apart. + // otherwise the physics engine will detect collisions and make them unstable. + float spacing = interleave + Physics.defaultContactOffset; + float x = offset + spawnX * spacing; + float y = spawnY * spacing; + + // instantiate & position + GameObject go = Instantiate(spawnPrefab); + go.transform.position = new Vector3(x, y, 0); + + // spawn + NetworkServer.Spawn(go); + ++spawned; + } + } + } + } + + public override void OnStartServer() + { + base.OnStartServer(); + SpawnAll(); + + // disable rendering on server to reduce noise in profiling. + // keep enabled in host mode though. + // if (mode == NetworkManagerMode.ServerOnly) + // Camera.main.enabled = false; + } + } +} diff --git a/Assets/Mirror/Examples/StackedPrediction/NetworkManagerStackedPrediction.cs.meta b/Assets/Mirror/Examples/StackedPrediction/NetworkManagerStackedPrediction.cs.meta new file mode 100644 index 000000000..8d6dd9705 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/NetworkManagerStackedPrediction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe94388660a5e45688a685723da14b57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/PlayerForce.cs b/Assets/Mirror/Examples/StackedPrediction/PlayerForce.cs new file mode 100644 index 000000000..08aea950f --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/PlayerForce.cs @@ -0,0 +1,47 @@ +// players can apply force to any stacked cube. +// this has to be on the player instead of on the cube via OnMouseDown, +// because OnMouseDown would get blocked by the predicted ghost objects. +using UnityEngine; + +namespace Mirror.Examples.PredictionBenchmark +{ + public class PlayerForce : NetworkBehaviour + { + public float force = 50; + + void Update() + { + if (!isLocalPlayer) return; + + if (Input.GetMouseButtonDown(0)) + { + // raycast into camera direction + Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); + if (Physics.Raycast(ray, out RaycastHit hit)) + { + // we may have hit the ghost object. + // find the original. + if (PredictedRigidbody.IsPredicted(hit.collider, out PredictedRigidbody predicted)) + { + // apply force in a random direction, this looks best + Debug.Log($"Applying force to: {hit.collider.name}"); + Vector3 impulse = Random.insideUnitSphere * force; + predicted.predictedRigidbody.AddForce(impulse, ForceMode.Impulse); + CmdApplyForce(predicted.netIdentity, impulse); + } + } + } + + } + + // every play can apply force to this object (no authority required) + [Command] + void CmdApplyForce(NetworkIdentity cube, Vector3 impulse) + { + // apply force in that direction + Debug.LogWarning($"CmdApplyForce: {force} to {cube.name}"); + Rigidbody rb = cube.GetComponent(); + rb.AddForce(impulse, ForceMode.Impulse); + } + } +} diff --git a/Assets/Mirror/Examples/StackedPrediction/PlayerForce.cs.meta b/Assets/Mirror/Examples/StackedPrediction/PlayerForce.cs.meta new file mode 100644 index 000000000..709b92efa --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/PlayerForce.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1a1438116a7f4afb89cf449850639d09 +timeCreated: 1711085217 \ No newline at end of file diff --git a/Assets/Mirror/Examples/StackedPrediction/PlayerSpectator.prefab b/Assets/Mirror/Examples/StackedPrediction/PlayerSpectator.prefab new file mode 100644 index 000000000..1e59a3423 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/PlayerSpectator.prefab @@ -0,0 +1,68 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6080703956733773953 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5774152995658786670} + - component: {fileID: 4958697633604052194} + - component: {fileID: -4308408187583617977} + m_Layer: 0 + m_Name: PlayerSpectator + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5774152995658786670 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6080703956733773953} + 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!114 &4958697633604052194 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6080703956733773953} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 3798687038 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &-4308408187583617977 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6080703956733773953} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a1438116a7f4afb89cf449850639d09, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + force: 50 diff --git a/Assets/Mirror/Examples/StackedPrediction/PlayerSpectator.prefab.meta b/Assets/Mirror/Examples/StackedPrediction/PlayerSpectator.prefab.meta new file mode 100644 index 000000000..efb744177 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/PlayerSpectator.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dc62ed4e37b7d4551b8c6a5edc0b36b4 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/PredictedCube.prefab b/Assets/Mirror/Examples/StackedPrediction/PredictedCube.prefab new file mode 100644 index 000000000..80b893cb7 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/PredictedCube.prefab @@ -0,0 +1,172 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1161087258037110271 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1161087258037110147} + - component: {fileID: 1161087258037110268} + - component: {fileID: 1161087258037110269} + - component: {fileID: 1161087258037110270} + - component: {fileID: 1161087258037110146} + - component: {fileID: 2515824601545922644} + - component: {fileID: -4200539020284490047} + m_Layer: 0 + m_Name: PredictedCube + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1161087258037110147 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + 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 &1161087258037110268 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &1161087258037110269 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + 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: 38254cb9b1a9c497385399ae2a4a6509, 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: 1 + 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!65 &1161087258037110270 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + m_Material: {fileID: 13400000, guid: 7341570099774486d9de82fa640e73ab, type: 2} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!54 &1161087258037110146 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + serializedVersion: 2 + m_Mass: 1 + m_Drag: 0 + m_AngularDrag: 0.05 + m_UseGravity: 1 + m_IsKinematic: 0 + m_Interpolate: 1 + m_Constraints: 0 + m_CollisionDetection: 1 +--- !u!114 &2515824601545922644 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 3288281219 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &-4200539020284490047 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1161087258037110271} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d38927cdc6024b9682b5fe9778b9ef99, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + predictedRigidbody: {fileID: 1161087258037110146} + mode: 1 + motionSmoothingVelocityThreshold: 0.1 + motionSmoothingAngularVelocityThreshold: 0.1 + motionSmoothingTimeTolerance: 0.5 + stateHistoryLimit: 32 + recordInterval: 0.05 + onlyRecordChanges: 1 + compareLastFirst: 1 + positionCorrectionThreshold: 0.1 + rotationCorrectionThreshold: 5 + oneFrameAhead: 1 + snapThreshold: 2 + showGhost: 0 + ghostVelocityThreshold: 0.1 + localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2} + remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, type: 2} + checkGhostsEveryNthFrame: 4 + positionInterpolationSpeed: 15 + rotationInterpolationSpeed: 10 + teleportDistanceMultiplier: 10 + reduceSendsWhileIdle: 1 diff --git a/Assets/Mirror/Examples/StackedPrediction/PredictedCube.prefab.meta b/Assets/Mirror/Examples/StackedPrediction/PredictedCube.prefab.meta new file mode 100644 index 000000000..8ed3ed6bc --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/PredictedCube.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8e024f835ef4842998861f956ca7cb5c +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/StackedPrediction/_Readme.txt b/Assets/Mirror/Examples/StackedPrediction/_Readme.txt new file mode 100644 index 000000000..d59db7f43 --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/_Readme.txt @@ -0,0 +1,17 @@ +This example is used to stabilize our prediction algorithm for stacked Rigidbodies. + +It's important to understand that there are two problems here: + +1. Stacking Rigidbodies with Unity physics. + This is difficult even in single player mode. + https://forum.unity.com/threads/stacking-boxes-issue.1341128/ + => with solverIterations=100 we can stack about 500 cubes at max. + +2. Networked Prediction for stacked Rigidbodies. + This is even harder since Rigidbodies may need to be corrected going through each other. + +==> This demo is NOT READY for users or for production games. +==> For now, this is only for the Mirror team to debug prediction. +==> Note that client cubes may change color if PredictedRigidbody.showRemoteSleeping is enabled. + +DO NOT USE THIS diff --git a/Assets/Mirror/Examples/StackedPrediction/_Readme.txt.meta b/Assets/Mirror/Examples/StackedPrediction/_Readme.txt.meta new file mode 100644 index 000000000..428bfa9eb --- /dev/null +++ b/Assets/Mirror/Examples/StackedPrediction/_Readme.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e7924ec6d89741879d02199175867d57 +timeCreated: 1711079004 \ No newline at end of file diff --git a/Assets/Mirror/Examples/_Common/Scripts/CanvasNetworkManagerHUD/CanvasNetworkManagerHUD.cs b/Assets/Mirror/Examples/_Common/Scripts/CanvasNetworkManagerHUD/CanvasNetworkManagerHUD.cs index a973411db..f85f78ec2 100755 --- a/Assets/Mirror/Examples/_Common/Scripts/CanvasNetworkManagerHUD/CanvasNetworkManagerHUD.cs +++ b/Assets/Mirror/Examples/_Common/Scripts/CanvasNetworkManagerHUD/CanvasNetworkManagerHUD.cs @@ -188,13 +188,13 @@ private void OnClientDisconnect() // you first add this script to a gameobject. private void Reset() { -#if UNITY_2021_3_OR_NEWER +#if UNITY_2022_2_OR_NEWER if (!FindAnyObjectByType()) Debug.LogError("This component requires a NetworkManager component to be present in the scene. Please add!"); #else - // Deprecated in Unity 2023.1 - if (!FindObjectOfType()) - Debug.LogError("This component requires a NetworkManager component to be present in the scene. Please add!"); + // Deprecated in Unity 2023.1 + if (!FindObjectOfType()) + Debug.LogError("This component requires a NetworkManager component to be present in the scene. Please add!"); #endif } } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs index 50d814ab9..9e6db124d 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs @@ -22,7 +22,12 @@ public static bool IsArmCPU() => public static BuildReport BuildServer() { - IEnumerable scenes = EditorBuildSettings.scenes.Select(s=>s.path); + // MIRROR CHANGE: only include scenes which are enabled + IEnumerable scenes = EditorBuildSettings.scenes + .Where(s => s.enabled) + .Select(s => s.path); + // END MIRROR CHANGE + BuildPlayerOptions options = new BuildPlayerOptions { scenes = scenes.ToArray(), diff --git a/Assets/Mirror/Plugins/BouncyCastle.meta b/Assets/Mirror/Plugins/BouncyCastle.meta new file mode 100644 index 000000000..e064e80ff --- /dev/null +++ b/Assets/Mirror/Plugins/BouncyCastle.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31ff83bf6d2e72542adcbe2c21383f4a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/BouncyCastle/BouncyCastle.Cryptography.dll b/Assets/Mirror/Plugins/BouncyCastle/BouncyCastle.Cryptography.dll new file mode 100644 index 000000000..9d9b6ac33 Binary files /dev/null and b/Assets/Mirror/Plugins/BouncyCastle/BouncyCastle.Cryptography.dll differ diff --git a/Assets/Mirror/Plugins/BouncyCastle/BouncyCastle.Cryptography.dll.meta b/Assets/Mirror/Plugins/BouncyCastle/BouncyCastle.Cryptography.dll.meta new file mode 100644 index 000000000..69befd70b --- /dev/null +++ b/Assets/Mirror/Plugins/BouncyCastle/BouncyCastle.Cryptography.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: a67bf078294b36e4686b9912bf172010 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/BouncyCastle/LICENSE.md b/Assets/Mirror/Plugins/BouncyCastle/LICENSE.md new file mode 100644 index 000000000..277dcd1eb --- /dev/null +++ b/Assets/Mirror/Plugins/BouncyCastle/LICENSE.md @@ -0,0 +1,13 @@ +Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org). +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sub license, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this +permission notice shall be included in all copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.** diff --git a/Assets/Mirror/Plugins/BouncyCastle/LICENSE.md.meta b/Assets/Mirror/Plugins/BouncyCastle/LICENSE.md.meta new file mode 100644 index 000000000..d0ce88350 --- /dev/null +++ b/Assets/Mirror/Plugins/BouncyCastle/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2b45a99b5583cda419e1f1ec943fec4b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Tests/Editor/Mirror.Tests.asmdef b/Assets/Mirror/Tests/Editor/Mirror.Tests.asmdef index 6e59124fd..9132b7df6 100644 --- a/Assets/Mirror/Tests/Editor/Mirror.Tests.asmdef +++ b/Assets/Mirror/Tests/Editor/Mirror.Tests.asmdef @@ -24,7 +24,8 @@ "Castle.Core.dll", "System.Threading.Tasks.Extensions.dll", "Mono.CecilX.dll", - "nunit.framework.dll" + "nunit.framework.dll", + "BouncyCastle.Cryptography.dll" ], "autoReferenced": false, "defineConstraints": [ diff --git a/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs b/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs index 290dd5824..7d75ae7c6 100644 --- a/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs +++ b/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs @@ -19,7 +19,10 @@ struct TestState : PredictedState public Vector3 velocity { get; set; } public Vector3 velocityDelta { get; set; } - public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vector3 velocity, Vector3 velocityDelta) + public Vector3 angularVelocity { get; set; } + public Vector3 angularVelocityDelta { get; set; } + + public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vector3 velocity, Vector3 velocityDelta, Vector3 angularVelocity, Vector3 angularVelocityDelta) { this.timestamp = timestamp; this.position = position; @@ -28,6 +31,8 @@ public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vect this.rotationDelta = Quaternion.identity; this.velocity = velocity; this.velocityDelta = velocityDelta; + this.angularVelocity = angularVelocity; + this.angularVelocityDelta = angularVelocityDelta; } } @@ -124,21 +129,21 @@ public void CorrectHistory() SortedList history = new SortedList(); // (0,0,0) with delta (0,0,0) from previous: - history.Add(0, new TestState(0, new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0))); + history.Add(0, new TestState(0, new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0))); // (1,0,0) with delta (1,0,0) from previous: - history.Add(1, new TestState(1, new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0))); + history.Add(1, new TestState(1, new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0))); // (2,0,0) with delta (1,0,0) from previous: - history.Add(2, new TestState(2, new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0))); + history.Add(2, new TestState(2, new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0))); // (3,0,0) with delta (1,0,0) from previous: - history.Add(3, new TestState(3, new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0))); + history.Add(3, new TestState(3, new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0))); // client receives a correction from server between t=1 and t=2. // exactly t=1.5 where position should be 1.5, server says it's +0.1 = 1.6 // deltas are zero because that's how PredictedBody.Serialize sends them, alwasy at zero. - TestState correction = new TestState(1.5, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero); + TestState correction = new TestState(1.5, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero); // Sample() will find that the value before correction is at t=1 and after at t=2. Assert.That(Prediction.Sample(history, correction.timestamp, out TestState before, out TestState after, out int afterIndex, out double t), Is.True); @@ -155,8 +160,10 @@ public void CorrectHistory() const int historyLimit = 32; Prediction.CorrectHistory(history, historyLimit, correction, before, after, afterIndex); - // there should be 4 initial + 1 corrected = 5 entries now - Assert.That(history.Count, Is.EqualTo(5)); + // PERFORMANCE OPTIMIZATION: nothing is inserted anymore, values are only adjusted. + // there should be 4 initial + 1 corrected = 5 entries now + // Assert.That(history.Count, Is.EqualTo(5)); + Assert.That(history.Count, Is.EqualTo(4)); // first entry at t=0 should be unchanged, since we corrected after that one. Assert.That(history.Keys[0], Is.EqualTo(0)); @@ -164,6 +171,8 @@ public void CorrectHistory() Assert.That(history.Values[0].positionDelta.x, Is.EqualTo(0)); Assert.That(history.Values[0].velocity.x, Is.EqualTo(0)); Assert.That(history.Values[0].velocityDelta.x, Is.EqualTo(0)); + Assert.That(history.Values[0].angularVelocity.x, Is.EqualTo(0)); + Assert.That(history.Values[0].angularVelocityDelta.x, Is.EqualTo(0)); // second entry at t=1 should be unchanged, since we corrected after that one. Assert.That(history.Keys[1], Is.EqualTo(1)); @@ -171,15 +180,20 @@ public void CorrectHistory() Assert.That(history.Values[1].positionDelta.x, Is.EqualTo(1)); Assert.That(history.Values[1].velocity.x, Is.EqualTo(1)); Assert.That(history.Values[1].velocityDelta.x, Is.EqualTo(1)); + Assert.That(history.Values[1].angularVelocity.x, Is.EqualTo(1)); + Assert.That(history.Values[1].angularVelocityDelta.x, Is.EqualTo(1)); - // third entry at t=1.5 should be the received state. - // absolute values should be the correction, without any deltas since - // server doesn't send those and we don't need them. - Assert.That(history.Keys[2], Is.EqualTo(1.5)); - Assert.That(history.Values[2].position.x, Is.EqualTo(1.6f).Within(0.001f)); - Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0)); - Assert.That(history.Values[2].velocity.x, Is.EqualTo(1.6f).Within(0.001f)); - Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0)); + // PERFORMANCE OPTIMIZATION: nothing is inserted anymore, values are only adjusted. + // third entry at t=1.5 should be the received state. + // absolute values should be the correction, without any deltas since + // server doesn't send those and we don't need them. + // Assert.That(history.Keys[2], Is.EqualTo(1.5)); + // Assert.That(history.Values[2].position.x, Is.EqualTo(1.6f).Within(0.001f)); + // Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0)); + // Assert.That(history.Values[2].velocity.x, Is.EqualTo(1.6f).Within(0.001f)); + // Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0)); + // Assert.That(history.Values[2].angularVelocity.x, Is.EqualTo(1.6f).Within(0.001f)); + // Assert.That(history.Values[2].angularVelocityDelta.x, Is.EqualTo(0)); // fourth entry at t=2: // delta was from t=1.0 @ 1 to t=2.0 @ 2 = 1.0 @@ -187,21 +201,25 @@ public void CorrectHistory() // the delta at t=1.5 would've been 0.5. // => the inserted position is at t=1.6 // => add the relative delta of 0.5 = 2.1 - Assert.That(history.Keys[3], Is.EqualTo(2.0)); - Assert.That(history.Values[3].position.x, Is.EqualTo(2.1).Within(0.001f)); - Assert.That(history.Values[3].positionDelta.x, Is.EqualTo(0.5).Within(0.001f)); - Assert.That(history.Values[3].velocity.x, Is.EqualTo(2.1).Within(0.001f)); - Assert.That(history.Values[3].velocityDelta.x, Is.EqualTo(0.5).Within(0.001f)); + Assert.That(history.Keys[2], Is.EqualTo(2.0)); + Assert.That(history.Values[2].position.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0.5).Within(0.001f)); + Assert.That(history.Values[2].velocity.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0.5).Within(0.001f)); + Assert.That(history.Values[2].angularVelocity.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[2].angularVelocityDelta.x, Is.EqualTo(0.5)); // fifth entry at t=3: // client moved by a delta of 1 here, and that remains unchanged. // absolute position was 3.0 but if we apply the delta of 1 to the one before at 2.1, // we get the new position of 3.1 - Assert.That(history.Keys[4], Is.EqualTo(3.0)); - Assert.That(history.Values[4].position.x, Is.EqualTo(3.1).Within(0.001f)); - Assert.That(history.Values[4].positionDelta.x, Is.EqualTo(1.0).Within(0.001f)); - Assert.That(history.Values[4].velocity.x, Is.EqualTo(3.1).Within(0.001f)); - Assert.That(history.Values[4].velocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Keys[3], Is.EqualTo(3.0)); + Assert.That(history.Values[3].position.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[3].positionDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Values[3].velocity.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[3].velocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Values[3].angularVelocity.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[3].angularVelocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); } } } diff --git a/Assets/Mirror/Tests/Editor/SyncCollections/SyncDictionaryTest.cs b/Assets/Mirror/Tests/Editor/SyncCollections/SyncDictionaryTest.cs index 61a93ebd8..1a41af07d 100644 --- a/Assets/Mirror/Tests/Editor/SyncCollections/SyncDictionaryTest.cs +++ b/Assets/Mirror/Tests/Editor/SyncCollections/SyncDictionaryTest.cs @@ -79,36 +79,132 @@ public void CurlyBracesConstructor() [Test] public void TestAdd() { + // Adds a new entry with index of 4 using .Add method +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncDictionary.Callback = (op, key, item) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_ADD)); + Assert.That(key, Is.EqualTo(4)); + Assert.That(item, Is.EqualTo("yay")); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncDictionary.OnAdd = (key) => + { + actionCalled = true; + Assert.That(key, Is.EqualTo(4)); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + }; + serverSyncDictionary.Add(4, "yay"); SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); Assert.That(clientSyncDictionary.ContainsKey(4)); Assert.That(clientSyncDictionary[4], Is.EqualTo("yay")); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void TestClear() { + // Verifies that the clear method works and that the data is still present for the Callback. +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncDictionary.Callback = (op, key, item) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_CLEAR)); + Assert.That(clientSyncDictionary.Count, Is.EqualTo(3)); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncDictionary.OnClear = () => + { + actionCalled = true; + Assert.That(clientSyncDictionary.Count, Is.EqualTo(3)); + }; + serverSyncDictionary.Clear(); SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); Assert.That(serverSyncDictionary, Is.EquivalentTo(new SyncDictionary())); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void TestSet() { + // Overwrites an existing entry +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncDictionary.Callback = (op, key, item) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_SET)); + Assert.That(key, Is.EqualTo(1)); + Assert.That(item, Is.EqualTo("yay")); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncDictionary.OnSet = (key, oldItem) => + { + actionCalled = true; + Assert.That(key, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + }; + serverSyncDictionary[1] = "yay"; SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); Assert.That(clientSyncDictionary.ContainsKey(1)); Assert.That(clientSyncDictionary[1], Is.EqualTo("yay")); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void TestBareSet() { + // Adds a new entry with index of 4 without using .Add method +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncDictionary.Callback = (op, key, item) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_ADD)); + Assert.That(key, Is.EqualTo(4)); + Assert.That(item, Is.EqualTo("yay")); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncDictionary.OnAdd = (key) => + { + actionCalled = true; + Assert.That(key, Is.EqualTo(4)); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + }; + serverSyncDictionary[4] = "yay"; SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); Assert.That(clientSyncDictionary.ContainsKey(4)); Assert.That(clientSyncDictionary[4], Is.EqualTo("yay")); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] @@ -172,52 +268,90 @@ public void TestContains() [Test] public void CallbackTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - clientSyncDictionary.Callback += (op, index, item) => + clientSyncDictionary.Callback = (op, key, item) => { called = true; Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_ADD)); - Assert.That(index, Is.EqualTo(3)); + Assert.That(key, Is.EqualTo(3)); Assert.That(item, Is.EqualTo("yay")); - Assert.That(clientSyncDictionary[index], Is.EqualTo("yay")); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncDictionary.OnAdd = (key) => + { + actionCalled = true; + Assert.That(key, Is.EqualTo(3)); + Assert.That(clientSyncDictionary[key], Is.EqualTo("yay")); + }; + serverSyncDictionary.Add(3, "yay"); SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void ServerCallbackTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - serverSyncDictionary.Callback += (op, index, item) => + serverSyncDictionary.Callback = (op, key, item) => { called = true; Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_ADD)); - Assert.That(index, Is.EqualTo(3)); + Assert.That(key, Is.EqualTo(3)); Assert.That(item, Is.EqualTo("yay")); - Assert.That(serverSyncDictionary[index], Is.EqualTo("yay")); + Assert.That(serverSyncDictionary[key], Is.EqualTo("yay")); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + serverSyncDictionary.OnAdd = (key) => + { + actionCalled = true; + Assert.That(key, Is.EqualTo(3)); + Assert.That(serverSyncDictionary[key], Is.EqualTo("yay")); + }; + serverSyncDictionary[3] = "yay"; Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void CallbackRemoveTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - clientSyncDictionary.Callback += (op, key, item) => + clientSyncDictionary.Callback = (op, key, item) => { called = true; Assert.That(op, Is.EqualTo(SyncDictionary.Operation.OP_REMOVE)); + Assert.That(key, Is.EqualTo(1)); Assert.That(item, Is.EqualTo("World")); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncDictionary.OnRemove = (key, oldItem) => + { + actionCalled = true; + Assert.That(key, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + Assert.That(!clientSyncDictionary.ContainsKey(1)); + }; + serverSyncDictionary.Remove(1); SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] diff --git a/Assets/Mirror/Tests/Editor/SyncCollections/SyncListStructTest.cs b/Assets/Mirror/Tests/Editor/SyncCollections/SyncListStructTest.cs index 4966f616b..63ba7cbe4 100644 --- a/Assets/Mirror/Tests/Editor/SyncCollections/SyncListStructTest.cs +++ b/Assets/Mirror/Tests/Editor/SyncCollections/SyncListStructTest.cs @@ -47,8 +47,9 @@ public void OldValueShouldNotBeNewValue() player.item.price = 15; serverList[0] = player; +#pragma warning disable 618 // Type or member is obsolete bool callbackCalled = false; - clientList.Callback += (SyncList.Operation op, int itemIndex, TestPlayer oldItem, TestPlayer newItem) => + clientList.Callback = (SyncList.Operation op, int itemIndex, TestPlayer oldItem, TestPlayer newItem) => { Assert.That(op == SyncList.Operation.OP_SET, Is.True); Assert.That(itemIndex, Is.EqualTo(0)); @@ -56,6 +57,7 @@ public void OldValueShouldNotBeNewValue() Assert.That(newItem.item.price, Is.EqualTo(15)); callbackCalled = true; }; +#pragma warning restore 618 // Type or member is obsolete SyncListTest.SerializeDeltaTo(serverList, clientList); Assert.IsTrue(callbackCalled); diff --git a/Assets/Mirror/Tests/Editor/SyncCollections/SyncListTest.cs b/Assets/Mirror/Tests/Editor/SyncCollections/SyncListTest.cs index 1d0d87a18..752237979 100644 --- a/Assets/Mirror/Tests/Editor/SyncCollections/SyncListTest.cs +++ b/Assets/Mirror/Tests/Editor/SyncCollections/SyncListTest.cs @@ -74,7 +74,7 @@ public void TestInit() [Test] public void CurlyBracesConstructor() { - SyncList list = new SyncList{1,2,3}; + SyncList list = new SyncList { 1, 2, 3 }; Assert.That(list.Count, Is.EqualTo(3)); } @@ -97,17 +97,58 @@ public void TestAddRange() [Test] public void TestClear() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_CLEAR)); + Assert.That(clientSyncList.Count, Is.EqualTo(3)); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnClear = () => + { + actionCalled = true; + Assert.That(clientSyncList.Count, Is.EqualTo(3)); + }; + serverSyncList.Clear(); SerializeDeltaTo(serverSyncList, clientSyncList); - Assert.That(clientSyncList, Is.EquivalentTo(new string[] {})); + Assert.That(clientSyncList, Is.EquivalentTo(new string[] { })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void TestInsert() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_INSERT)); + Assert.That(index, Is.EqualTo(0)); + Assert.That(newItem, Is.EqualTo("yay")); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnInsert = (index) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(0)); + }; + serverSyncList.Insert(0, "yay"); SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "yay", "Hello", "World", "!" })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] @@ -121,19 +162,74 @@ public void TestInsertRange() [Test] public void TestSet() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_SET)); + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + Assert.That(newItem, Is.EqualTo("yay")); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnSet = (index, oldItem) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + }; + serverSyncList[1] = "yay"; SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList[1], Is.EqualTo("yay")); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "Hello", "yay", "!" })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void TestSetNull() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_SET)); + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + Assert.That(newItem, Is.EqualTo(null)); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnSet = (index, oldItem) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + }; + serverSyncList[1] = null; SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList[1], Is.EqualTo(null)); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "Hello", null, "!" })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); + +#pragma warning disable 618 // Type or member is obsolete + // clear callback so we don't get called again + clientSyncList.Callback = null; +#pragma warning restore 618 // Type or member is obsolete + + // clear handlers so we don't get called again + clientSyncList.OnSet = null; + serverSyncList[1] = "yay"; SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "Hello", "yay", "!" })); @@ -142,9 +238,33 @@ public void TestSetNull() [Test] public void TestRemoveAll() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_REMOVEAT)); + Assert.That(index, Is.EqualTo(0)); + Assert.That(oldItem, Is.Not.EqualTo("!")); + Assert.That(newItem, Is.EqualTo(default(string))); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnRemove = (index, item) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(0)); + Assert.That(item, Is.Not.EqualTo("!")); + }; + + // This will remove "Hello" and "World" serverSyncList.RemoveAll(entry => entry.Contains("l")); SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "!" })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] @@ -158,17 +278,63 @@ public void TestRemoveAllNone() [Test] public void TestRemoveAt() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_REMOVEAT)); + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + Assert.That(newItem, Is.EqualTo(default(string))); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnRemove = (index, oldItem) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + }; + serverSyncList.RemoveAt(1); SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "Hello", "!" })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void TestRemove() { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncList.Callback = (op, index, oldItem, newItem) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncList.Operation.OP_REMOVEAT)); + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + Assert.That(newItem, Is.EqualTo(default(string))); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnRemove = (index, oldItem) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + }; + serverSyncList.Remove("World"); SerializeDeltaTo(serverSyncList, clientSyncList); Assert.That(clientSyncList, Is.EquivalentTo(new[] { "Hello", "!" })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] @@ -276,9 +442,9 @@ public void SyncListFloatTest() [Test] public void CallbackTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - - clientSyncList.Callback += (op, index, oldItem, newItem) => + clientSyncList.Callback = (op, index, oldItem, newItem) => { called = true; @@ -287,19 +453,28 @@ public void CallbackTest() Assert.That(oldItem, Is.EqualTo(default(string))); Assert.That(newItem, Is.EqualTo("yay")); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnAdd = (index) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(3)); + Assert.That(clientSyncList[index], Is.EqualTo("yay")); + }; serverSyncList.Add("yay"); SerializeDeltaTo(serverSyncList, clientSyncList); - Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void CallbackRemoveTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - - clientSyncList.Callback += (op, index, oldItem, newItem) => + clientSyncList.Callback = (op, index, oldItem, newItem) => { called = true; @@ -307,18 +482,28 @@ public void CallbackRemoveTest() Assert.That(oldItem, Is.EqualTo("World")); Assert.That(newItem, Is.EqualTo(default(string))); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnRemove = (index, oldItem) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + }; + serverSyncList.Remove("World"); SerializeDeltaTo(serverSyncList, clientSyncList); - Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void CallbackRemoveAtTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - - clientSyncList.Callback += (op, index, oldItem, newItem) => + clientSyncList.Callback = (op, index, oldItem, newItem) => { called = true; @@ -327,11 +512,20 @@ public void CallbackRemoveAtTest() Assert.That(oldItem, Is.EqualTo("World")); Assert.That(newItem, Is.EqualTo(default(string))); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncList.OnRemove = (index, oldItem) => + { + actionCalled = true; + Assert.That(index, Is.EqualTo(1)); + Assert.That(oldItem, Is.EqualTo("World")); + }; serverSyncList.RemoveAt(1); SerializeDeltaTo(serverSyncList, clientSyncList); - Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] diff --git a/Assets/Mirror/Tests/Editor/SyncCollections/SyncSetTest.cs b/Assets/Mirror/Tests/Editor/SyncCollections/SyncSetTest.cs index f9021d204..166c7c522 100644 --- a/Assets/Mirror/Tests/Editor/SyncCollections/SyncSetTest.cs +++ b/Assets/Mirror/Tests/Editor/SyncCollections/SyncSetTest.cs @@ -66,7 +66,7 @@ public void TestInit() [Test] public void CurlyBracesConstructor() { - SyncHashSet set = new SyncHashSet{1,2,3}; + SyncHashSet set = new SyncHashSet { 1, 2, 3 }; Assert.That(set.Count, Is.EqualTo(3)); } @@ -79,14 +79,6 @@ public void TestAdd() Assert.That(clientSyncSet, Is.EquivalentTo(new[] { "Hello", "World", "!", "yay" })); } - [Test] - public void TestClear() - { - serverSyncSet.Clear(); - SerializeDeltaTo(serverSyncSet, clientSyncSet); - Assert.That(clientSyncSet, Is.EquivalentTo(new string[] {})); - } - [Test] public void TestRemove() { @@ -96,6 +88,34 @@ public void TestRemove() Assert.That(clientSyncSet, Is.EquivalentTo(new[] { "Hello", "!" })); } + [Test] + public void TestClear() + { +#pragma warning disable 618 // Type or member is obsolete + bool called = false; + clientSyncSet.Callback = (op, item) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncHashSet.Operation.OP_CLEAR)); + Assert.That(clientSyncSet.Count, Is.EqualTo(3)); + }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncSet.OnClear = () => + { + actionCalled = true; + Assert.That(clientSyncSet.Count, Is.EqualTo(3)); + }; + + serverSyncSet.Clear(); + SerializeDeltaTo(serverSyncSet, clientSyncSet); + Assert.That(clientSyncSet, Is.EquivalentTo(new string[] { })); + Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); + } + [Test] public void TestMultSync() { @@ -110,38 +130,55 @@ public void TestMultSync() [Test] public void CallbackTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - - clientSyncSet.Callback += (op, item) => + clientSyncSet.Callback = (op, item) => { called = true; Assert.That(op, Is.EqualTo(SyncHashSet.Operation.OP_ADD)); Assert.That(item, Is.EqualTo("yay")); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncSet.OnAdd = (item) => + { + actionCalled = true; + Assert.That(item, Is.EqualTo("yay")); + }; serverSyncSet.Add("yay"); SerializeDeltaTo(serverSyncSet, clientSyncSet); - Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] public void CallbackRemoveTest() { +#pragma warning disable 618 // Type or member is obsolete bool called = false; - - clientSyncSet.Callback += (op, item) => + clientSyncSet.Callback = (op, item) => { called = true; Assert.That(op, Is.EqualTo(SyncHashSet.Operation.OP_REMOVE)); Assert.That(item, Is.EqualTo("World")); }; +#pragma warning restore 618 // Type or member is obsolete + + bool actionCalled = false; + clientSyncSet.OnRemove = (oldItem) => + { + actionCalled = true; + Assert.That(oldItem, Is.EqualTo("World")); + }; + serverSyncSet.Remove("World"); SerializeDeltaTo(serverSyncSet, clientSyncSet); - Assert.That(called, Is.True); + Assert.That(actionCalled, Is.True); } [Test] @@ -162,7 +199,7 @@ public void TestExceptWithSelf() { serverSyncSet.ExceptWith(serverSyncSet); SerializeDeltaTo(serverSyncSet, clientSyncSet); - Assert.That(clientSyncSet, Is.EquivalentTo(new String[] {})); + Assert.That(clientSyncSet, Is.EquivalentTo(new String[] { })); } [Test] @@ -242,7 +279,7 @@ public void TestSymmetricExceptWithSelf() { serverSyncSet.SymmetricExceptWith(serverSyncSet); SerializeDeltaTo(serverSyncSet, clientSyncSet); - Assert.That(clientSyncSet, Is.EquivalentTo(new String[] {})); + Assert.That(clientSyncSet, Is.EquivalentTo(new String[] { })); } [Test] diff --git a/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportConnectionTest.cs b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportConnectionTest.cs new file mode 100644 index 000000000..418d9bc5b --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportConnectionTest.cs @@ -0,0 +1,551 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Mirror.Tests.NetworkServers; +using Mirror.Transports.Encryption; +using NUnit.Framework; + +namespace Mirror.Tests.Transports +{ + public class EncryptionTransportConnectionTest + { + struct Data + { + public byte[] data; + public int channel; + } + private EncryptedConnection server; + private EncryptionCredentials serverCreds; + Queue serverRecv = new Queue(); + private Action serverReady; + private Action, int> serverReceive; + private Func, int, bool> shouldServerSend; + private Func serverValidateKey; + + private EncryptedConnection client; + private EncryptionCredentials clientCreds; + Queue clientRecv = new Queue(); + private Action clientReady; + private Action, int> clientReceive; + private Func, int, bool> shouldClientSend; + private Func clientValidateKey; + + private double _time; + private double _timestep = 0.05; + class ErrorException : Exception + { + public ErrorException(string msg) : base(msg) {} + } + + [SetUp] + public void Setup() + { + serverReady = null; + serverReceive = null; + shouldServerSend = null; + serverValidateKey = null; + clientReady = null; + clientReceive = null; + shouldClientSend = null; + clientValidateKey = null; + clientRecv.Clear(); + serverRecv.Clear(); + _time = 0; + + serverCreds = EncryptionCredentials.Generate(); + server = new EncryptedConnection(serverCreds, false, + (bytes, channel) => + { + if (shouldServerSend == null || shouldServerSend(bytes, channel)) + clientRecv.Enqueue(new Data + { + data = bytes.ToArray(), channel = channel + }); + }, + (bytes, channel) => + { + serverReceive?.Invoke(bytes, channel); + }, + () => { serverReady?.Invoke(); }, + (error, s) => throw new ErrorException($"{error}: {s}"), + info => + { + if (serverValidateKey != null) return serverValidateKey(info); + return true; + }); + + clientCreds = EncryptionCredentials.Generate(); + client = new EncryptedConnection(clientCreds, true, + (bytes, channel) => + { + if (shouldClientSend == null || shouldClientSend(bytes, channel)) + serverRecv.Enqueue(new Data + { + data = bytes.ToArray(), channel = channel + }); + }, + (bytes, channel) => + { + clientReceive?.Invoke(bytes, channel); + }, + () => { clientReady?.Invoke(); }, + (error, s) => throw new ErrorException($"{error}: {s}. t={_time}"), + info => + { + if (clientValidateKey != null) return clientValidateKey(info); + return true; + }); + } + + private void Pump() + { + _time += _timestep; + + while (clientRecv.TryDequeue(out Data data)) + { + client.OnReceiveRaw(new ArraySegment(data.data), data.channel); + } + if (!client.IsReady) + { + client.TickNonReady(_time); + } + + while (serverRecv.TryDequeue(out Data data)) + { + server.OnReceiveRaw(new ArraySegment(data.data), data.channel); + } + if (!server.IsReady) + { + server.TickNonReady(_time); + } + } + + [TearDown] + public void TearDown() + { + } + + [Test] + public void TestHandshakeSuccess() + { + bool isServerReady = false; + bool isClientReady = false; + clientReady = () => + { + Assert.False(isClientReady); // only called once + Assert.True(client.IsReady); // should be set when called + isClientReady = true; + }; + serverReady = () => + { + Assert.False(isServerReady); // only called once + Assert.True(server.IsReady); // should be set when called + isServerReady = true; + server.Send(new ArraySegment(new byte[] + { + 1, 2, 3 + }), Channels.Reliable); // need to send to ready the other side + }; + + while (!isServerReady || !isClientReady) + { + if (_time > 20) + { + throw new Exception("Timeout."); + } + Pump(); + } + } + + [Test] + public void TestHandshakeSuccessWithLoss() + { + int clientCount = 0; + shouldClientSend = (data, channel) => + { + if (channel == Channels.Unreliable) + { + clientCount++; + // drop 75% of packets + return clientCount % 4 == 0; + } + return true; + }; + int serverCount = 0; + shouldServerSend = (data, channel) => + { + if (channel == Channels.Unreliable) + { + serverCount++; + // drop 75% of packets + return serverCount % 4 == 0; + } + return true; + }; + TestHandshakeSuccess(); + } + + private bool ArrayContainsSequence(ArraySegment haystack, ArraySegment needle) + { + if (needle.Count == 0) + { + return true; + } + int ni = 0; + for (int hi = 0; hi < haystack.Count; hi++) + { + if (haystack.Array[haystack.Offset + hi] == needle.Array[needle.Offset + ni]) + { + ni++; + if (ni == needle.Count) + { + return true; + } + } + else + { + ni = 0; + } + } + return false; + } + + [Test] + public void TestUtil() + { + Assert.True(ArrayContainsSequence(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), new ArraySegment(new byte[] + { + }))); + Assert.True(ArrayContainsSequence(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }))); + Assert.True(ArrayContainsSequence(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), new ArraySegment(new byte[] + { + 2, 3 + }))); + Assert.True(ArrayContainsSequence(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), new ArraySegment(new byte[] + { + 3, 4 + }))); + Assert.False(ArrayContainsSequence(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), new ArraySegment(new byte[] + { + 1, 3 + }))); + Assert.False(ArrayContainsSequence(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), new ArraySegment(new byte[] + { + 3, 4, 5 + }))); + + } + [Test] + public void TestDataSecurity() + { + byte[] serverData = Encoding.UTF8.GetBytes("This is very important secret server data"); + byte[] clientData = Encoding.UTF8.GetBytes("Super secret data from the client is contained within."); + bool isServerDone = false; + bool isClientDone = false; + clientReady = () => + { + client.Send(new ArraySegment(clientData), Channels.Reliable); + }; + serverReady = () => + { + server.Send(new ArraySegment(serverData), Channels.Reliable); + }; + + shouldServerSend = (bytes, i) => + { + if (i == Channels.Reliable) + { + Assert.False(ArrayContainsSequence(bytes, new ArraySegment(serverData))); + } + return true; + }; + shouldClientSend = (bytes, i) => + { + if (i == Channels.Reliable) + { + Assert.False(ArrayContainsSequence(bytes, new ArraySegment(clientData))); + } + return true; + }; + + serverReceive = (bytes, channel) => + { + Assert.AreEqual(Channels.Reliable, channel); + Assert.AreEqual(bytes, new ArraySegment(clientData)); + Assert.False(isServerDone); + isServerDone = true; + }; + clientReceive = (bytes, channel) => + { + Assert.AreEqual(Channels.Reliable, channel); + Assert.AreEqual(bytes, new ArraySegment(serverData)); + Assert.False(isClientDone); + isClientDone = true; + }; + + while (!isServerDone || !isClientDone) + { + if (_time > 20) + { + throw new Exception("Timeout."); + } + Pump(); + } + } + + [Test] + public void TestBadOpCodeErrors() + { + Assert.Throws(() => + { + shouldServerSend = (bytes, i) => + { + // mess up the opcode (first byte) + bytes.Array[bytes.Offset] += 0xAA; + return true; + }; + // setup + TestHandshakeSuccess(); + }); + } + [Test] + public void TestEarlyDataOpCodeErrors() + { + Assert.Throws(() => + { + shouldServerSend = (bytes, i) => + { + // mess up the opcode (first byte) + bytes.Array[bytes.Offset] = 1; // data + return true; + }; + // setup + TestHandshakeSuccess(); + }); + } + + [Test] + public void TestUnexpectedAckOpCodeErrors() + { + Assert.Throws(() => + { + shouldServerSend = (bytes, i) => + { + // mess up the opcode (first byte) + bytes.Array[bytes.Offset] = 2; // start, client doesn't expect this + return true; + }; + // setup + TestHandshakeSuccess(); + }); + } + + [Test] + public void TestUnexpectedHandshakeOpCodeErrors() + { + Assert.Throws(() => + { + shouldClientSend = (bytes, i) => + { + // mess up the opcode (first byte) + bytes.Array[bytes.Offset] = 3; // ack, server doesn't expect this + return true; + }; + // setup + TestHandshakeSuccess(); + }); + } + [Test] + public void TestUnexpectedFinOpCodeErrors() + { + Assert.Throws(() => + { + shouldServerSend = (bytes, i) => + { + // mess up the opcode (first byte) + bytes.Array[bytes.Offset] = 4; // fin, client doesn't expect this + return true; + }; + // setup + TestHandshakeSuccess(); + }); + } + [Test] + public void TestBadDataErrors() + { + TestHandshakeSuccess(); + Assert.Throws(() => + { + // setup + shouldServerSend = (bytes, i) => + { + // mess up a byte in the data + bytes.Array[bytes.Offset + 3] += 1; + return true; + }; + server.Send(new ArraySegment(new byte[] + { + 1, 2, 3, 4 + }), Channels.Reliable); + Pump(); + }); + } + + [Test] + public void TestBadPubKeyInStartErrors() + { + shouldClientSend = (bytes, i) => + { + if (bytes.Array[bytes.Offset] == 2 /* HandshakeStart Opcode */) + { + // mess up a byte in the data + bytes.Array[bytes.Offset + 3] += 1; + } + return true; + }; + Assert.Throws(() => + { + TestHandshakeSuccess(); + }); + } + + [Test] + public void TestBadPubKeyInAckErrors() + { + shouldServerSend = (bytes, i) => + { + if (bytes.Array[bytes.Offset] == 3 /* HandshakeAck Opcode */) + { + // mess up a byte in the data + bytes.Array[bytes.Offset + 3] += 1; + } + return true; + }; + Assert.Throws(() => + { + TestHandshakeSuccess(); + }); + } + + [Test] + public void TestDataSizes() + { + List sizes = new List(); + sizes.Add(1); + sizes.Add(2); + sizes.Add(3); + sizes.Add(6); + sizes.Add(9); + sizes.Add(16); + sizes.Add(60); + sizes.Add(100); + sizes.Add(200); + sizes.Add(400); + sizes.Add(800); + sizes.Add(1024); + sizes.Add(1025); + sizes.Add(4096); + sizes.Add(1024 * 16); + sizes.Add(1024 * 64); + sizes.Add(1024 * 128); + sizes.Add(1024 * 512); + // removed for performance, these do pass though + //sizes.Add(1024 * 1024); + //sizes.Add(1024 * 1024 * 16); + //sizes.Add(1024 * 1024 * 64); // 64MiB + + TestHandshakeSuccess(); + var maxSize = sizes.Max(); + var sendByte = new byte[maxSize]; + for (uint i = 0; i < sendByte.Length; i++) + { + sendByte[i] = (byte)i; + } + int size = -1; + clientReceive = (bytes, channel) => + { + // Assert.AreEqual is super slow for larger arrays, so do it manually + Assert.AreEqual(bytes.Count, size); + for (int i = 0; i < size; i++) + { + if (bytes.Array[bytes.Offset + i] != sendByte[i]) + { + Assert.Fail($"received bytes[{i}] did not match. expected {sendByte[i]}, got {bytes.Array[bytes.Offset + i]}"); + } + } + }; + foreach (var s in sizes) + { + size = s; + server.Send(new ArraySegment(sendByte, 0, size), 1); + Pump(); + } + } + + + [Test] + public void TestPubKeyValidationIsCalled() + { + bool clientCalled = false; + clientValidateKey = info => + { + Assert.AreEqual(new ArraySegment(serverCreds.PublicKeySerialized), info.Serialized); + Assert.AreEqual(serverCreds.PublicKeyFingerprint, info.Fingerprint); + clientCalled = true; + return true; + }; + bool serverCalled = false; + serverValidateKey = info => + { + Assert.AreEqual(clientCreds.PublicKeyFingerprint, info.Fingerprint); + serverCalled = true; + return true; + }; + TestHandshakeSuccess(); + Assert.IsTrue(clientCalled); + Assert.IsTrue(serverCalled); + } + + [Test] + public void TestClientPubKeyValidationErrors() + { + clientValidateKey = info => false; + Assert.Throws(() => + { + TestHandshakeSuccess(); + }); + } + + [Test] + public void TestServerPubKeyValidationErrors() + { + serverValidateKey = info => false; + Assert.Throws(() => + { + TestHandshakeSuccess(); + }); + } + } +} diff --git a/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportConnectionTest.cs.meta b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportConnectionTest.cs.meta new file mode 100644 index 000000000..438a37a4c --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportConnectionTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6132bc4b559a42b88bd94cc25e1390bf +timeCreated: 1708170265 \ No newline at end of file diff --git a/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportTransportTest.cs b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportTransportTest.cs new file mode 100644 index 000000000..269ce1b15 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportTransportTest.cs @@ -0,0 +1,234 @@ +using System; +using Mirror.Transports.Encryption; +using NSubstitute; +using NUnit.Framework; +using UnityEngine; + +namespace Mirror.Tests.Transports +{ + + // This is mostly a copy of MiddlewareTransport, with the stuff requiring actual connections to be setup deleted + [Description("Test to make sure inner methods are called when using Encryption Transport")] + public class EncryptionTransportTransportTest + { + Transport inner; + EncryptionTransport encryption; + + [SetUp] + public void Setup() + { + inner = Substitute.For(); + + GameObject gameObject = new GameObject(); + + encryption = gameObject.AddComponent(); + encryption.inner = inner; + } + + [TearDown] + public void TearDown() + { + GameObject.DestroyImmediate(encryption.gameObject); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void TestAvailable(bool available) + { + inner.Available().Returns(available); + + Assert.That(encryption.Available(), Is.EqualTo(available)); + + inner.Received(1).Available(); + } + + [Test] + [TestCase(Channels.Reliable, 4000)] + [TestCase(Channels.Reliable, 2000)] + [TestCase(Channels.Unreliable, 4000)] + public void TestGetMaxPacketSize(int channel, int packageSize) + { + inner.GetMaxPacketSize(Arg.Any()).Returns(packageSize); + + Assert.That(encryption.GetMaxPacketSize(channel), Is.EqualTo(packageSize - EncryptedConnection.Overhead)); + + inner.Received(1).GetMaxPacketSize(Arg.Is(x => x == channel)); + inner.Received(0).GetMaxPacketSize(Arg.Is(x => x != channel)); + } + + [Test] + public void TestShutdown() + { + encryption.Shutdown(); + + inner.Received(1).Shutdown(); + } + + [Test] + [TestCase("localhost")] + [TestCase("example.com")] + public void TestClientConnect(string address) + { + encryption.ClientConnect(address); + + inner.Received(1).ClientConnect(address); + inner.Received(0).ClientConnect(Arg.Is(x => x != address)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void TestClientConnected(bool connected) + { + inner.ClientConnected().Returns(connected); + + Assert.That(encryption.ClientConnected(), Is.EqualTo(false)); // not testing connection handshaking here + } + + [Test] + public void TestClientDisconnect() + { + encryption.ClientDisconnect(); + + inner.Received(1).ClientDisconnect(); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void TestServerActive(bool active) + { + inner.ServerActive().Returns(active); + + Assert.That(encryption.ServerActive(), Is.EqualTo(active)); + + inner.Received(1).ServerActive(); + } + + [Test] + public void TestServerStart() + { + encryption.ServerStart(); + + inner.Received(1).ServerStart(); + } + + [Test] + public void TestServerStop() + { + encryption.ServerStop(); + + inner.Received(1).ServerStop(); + } + + [Test] + [TestCase(0, "tcp4://localhost:7777")] + [TestCase(19, "tcp4://example.com:7777")] + public void TestServerGetClientAddress(int id, string result) + { + inner.ServerGetClientAddress(id).Returns(result); + + Assert.That(encryption.ServerGetClientAddress(id), Is.EqualTo(result)); + + inner.Received(1).ServerGetClientAddress(id); + inner.Received(0).ServerGetClientAddress(Arg.Is(x => x != id)); + + } + + [Test] + [TestCase("tcp4://localhost:7777")] + [TestCase("tcp4://example.com:7777")] + public void TestServerUri(string address) + { + Uri uri = new Uri(address); + inner.ServerUri().Returns(uri); + + Assert.That(encryption.ServerUri(), Is.EqualTo(uri)); + + inner.Received(1).ServerUri(); + } + + [Test] + public void TestClientDisconnectedCallback() + { + int called = 0; + encryption.OnClientDisconnected = () => + { + called++; + }; + // connect to give callback to inner + encryption.ClientConnect("localhost"); + + inner.OnClientDisconnected.Invoke(); + Assert.That(called, Is.EqualTo(1)); + + inner.OnClientDisconnected.Invoke(); + Assert.That(called, Is.EqualTo(2)); + } + + [Test] + public void TestClientErrorCallback() + { + int called = 0; + encryption.OnClientError = (error, reason) => + { + called++; + Assert.That(error, Is.EqualTo(TransportError.Unexpected)); + }; + // connect to give callback to inner + encryption.ClientConnect("localhost"); + + inner.OnClientError.Invoke(TransportError.Unexpected, ""); + Assert.That(called, Is.EqualTo(1)); + + inner.OnClientError.Invoke(TransportError.Unexpected, ""); + Assert.That(called, Is.EqualTo(2)); + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(19)] + public void TestServerDisconnectedCallback(int id) + { + int called = 0; + encryption.OnServerDisconnected = (i) => + { + called++; + Assert.That(i, Is.EqualTo(id)); + }; + // start to give callback to inner + encryption.ServerStart(); + + inner.OnServerDisconnected.Invoke(id); + Assert.That(called, Is.EqualTo(1)); + + inner.OnServerDisconnected.Invoke(id); + Assert.That(called, Is.EqualTo(2)); + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(19)] + public void TestServerErrorCallback(int id) + { + int called = 0; + encryption.OnServerError = (i, error, reason) => + { + called++; + Assert.That(i, Is.EqualTo(id)); + Assert.That(error, Is.EqualTo(TransportError.Unexpected)); + }; + // start to give callback to inner + encryption.ServerStart(); + + inner.OnServerError.Invoke(id, TransportError.Unexpected, ""); + Assert.That(called, Is.EqualTo(1)); + + inner.OnServerError.Invoke(id, TransportError.Unexpected, ""); + Assert.That(called, Is.EqualTo(2)); + } + } +} diff --git a/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportTransportTest.cs.meta b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportTransportTest.cs.meta new file mode 100644 index 000000000..8dea8ee28 --- /dev/null +++ b/Assets/Mirror/Tests/Editor/Transports/EncryptionTransportTransportTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 24f6aad90a7a4a42bba0473d5b27ebe8 +timeCreated: 1708386085 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta new file mode 100644 index 000000000..5621c2065 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 447b4ad1a3db7cf4fa5a0709d297ba9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs new file mode 100644 index 000000000..5eff64632 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections; +using System.Threading; +using Mirror; +using UnityEngine; +using Random = UnityEngine.Random; +namespace Edgegap +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")] + public class EdgegapLobbyKcpTransport : EdgegapKcpTransport + { + [Header("Lobby Settings")] + [Tooltip("URL to the Edgegap lobby service, automatically filled in after completing the creation process via button below (or enter manually)")] + public string lobbyUrl; + [Tooltip("How long to wait for the relay to be assigned after starting a lobby")] + public float lobbyWaitTimeout = 60; + + public LobbyApi Api; + private LobbyCreateRequest? _request; + private string _lobbyId; + private string _playerId; + private TransportStatus _status = TransportStatus.Offline; + public enum TransportStatus + { + Offline, + CreatingLobby, + StartingLobby, + JoiningLobby, + WaitingRelay, + Connecting, + Connected, + Error, + } + public TransportStatus Status + { + get + { + if (!NetworkClient.active && !NetworkServer.active) + { + return TransportStatus.Offline; + } + if (_status == TransportStatus.Connecting) + { + if (NetworkServer.active) + { + switch (((EdgegapKcpServer)this.server).state) + { + case ConnectionState.Valid: + return TransportStatus.Connected; + case ConnectionState.Invalid: + case ConnectionState.SessionTimeout: + case ConnectionState.Error: + return TransportStatus.Error; + } + } + else if (NetworkClient.active) + { + switch (((EdgegapKcpClient)this.client).connectionState) + { + case ConnectionState.Valid: + return TransportStatus.Connected; + case ConnectionState.Invalid: + case ConnectionState.SessionTimeout: + case ConnectionState.Error: + return TransportStatus.Error; + } + } + } + return _status; + } + } + + protected override void Awake() + { + base.Awake(); + Api = new LobbyApi(lobbyUrl); + } + + private void Reset() + { + this.relayGUI = false; + } + + public override void ServerStart() + { + if (!_request.HasValue) + { + throw new Exception("No lobby request set. Call SetServerLobbyParams"); + } + _status = TransportStatus.CreatingLobby; + Api.CreateLobby(_request.Value, lobby => + { + _lobbyId = lobby.lobby_id; + _status = TransportStatus.StartingLobby; + Api.StartLobby(new LobbyIdRequest(_lobbyId), () => + { + StartCoroutine(WaitForLobbyRelay(_lobbyId, true)); + }, error => + { + _status = TransportStatus.Error; + string errorMsg = $"Could not start lobby: {error}"; + Debug.LogError(errorMsg); + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + }); + }, + error => + { + _status = TransportStatus.Error; + string errorMsg = $"Couldn't create lobby: {error}"; + Debug.LogError(errorMsg); + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + }); + } + + public override void ServerStop() + { + base.ServerStop(); + + Api.DeleteLobby(_lobbyId, () => + { + // yay + }, error => + { + OnServerError?.Invoke(0, TransportError.Unexpected, $"Failed to delete lobby: {error}"); + }); + } + + public override void ClientDisconnect() + { + base.ClientDisconnect(); + // this gets called for host mode as well + if (!NetworkServer.active) + { + Api.LeaveLobby(new LobbyJoinOrLeaveRequest + { + player = new LobbyJoinOrLeaveRequest.Player + { + id = _playerId + }, + lobby_id = _lobbyId + }, () => + { + // yay + }, error => + { + string errorMsg = $"Failed to leave lobby: {error}"; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + Debug.LogError(errorMsg); + }); + } + } + + public override void ClientConnect(string address) + { + _lobbyId = address; + _playerId = RandomPlayerId(); + _status = TransportStatus.JoiningLobby; + Api.JoinLobby(new LobbyJoinOrLeaveRequest + { + player = new LobbyJoinOrLeaveRequest.Player + { + id = _playerId, + }, + lobby_id = address + }, () => + { + StartCoroutine(WaitForLobbyRelay(_lobbyId, false)); + }, error => + { + _status = TransportStatus.Offline; + string errorMsg = $"Failed to join lobby: {error}"; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + Debug.LogError(errorMsg); + OnClientDisconnected?.Invoke(); + }); + } + + private IEnumerator WaitForLobbyRelay(string lobbyId, bool forServer) + { + _status = TransportStatus.WaitingRelay; + double startTime = NetworkTime.localTime; + bool running = true; + while (running) + { + if (NetworkTime.localTime - startTime >= lobbyWaitTimeout) + { + _status = TransportStatus.Error; + string errorMsg = "Timed out waiting for lobby."; + Debug.LogError(errorMsg); + if (forServer) + { + _status = TransportStatus.Error; + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + _status = TransportStatus.Error; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + yield break; + } + bool waitingForResponse = true; + Api.GetLobby(lobbyId, lobby => + { + waitingForResponse = false; + if (!string.IsNullOrEmpty(lobby.assignment.ip)) + { + relayAddress = lobby.assignment.ip; + foreach (Lobby.Port aport in lobby.assignment.ports) + { + if (aport.protocol == "UDP") + { + if (aport.name == "server") + { + relayGameServerPort = (ushort)aport.port; + + } + else if (aport.name == "client") + { + relayGameClientPort = (ushort)aport.port; + } + } + } + bool found = false; + foreach (Lobby.Player player in lobby.players) + { + if (player.id == _playerId) + { + userId = player.authorization_token; + sessionId = lobby.assignment.authorization_token; + found = true; + break; + } + } + running = false; + if (!found) + { + string errorMsg = $"Couldn't find my player ({_playerId})"; + Debug.LogError(errorMsg); + + if (forServer) + { + _status = TransportStatus.Error; + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + _status = TransportStatus.Error; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + } + _status = TransportStatus.Connecting; + if (forServer) + { + base.ServerStart(); + } + else + { + base.ClientConnect(""); + } + } + }, error => + { + running = false; + waitingForResponse = false; + _status = TransportStatus.Error; + string errorMsg = $"Failed to get lobby info: {error}"; + Debug.LogError(errorMsg); + if (forServer) + { + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + }); + while (waitingForResponse) + { + yield return null; + } + yield return new WaitForSeconds(0.2f); + } + } + private static string RandomPlayerId() + { + return $"mirror-player-{Random.Range(1, int.MaxValue)}"; + } + + public void SetServerLobbyParams(string lobbyName, int capacity) + { + SetServerLobbyParams(new LobbyCreateRequest + { + player = new LobbyCreateRequest.Player + { + id = RandomPlayerId(), + }, + annotations = new LobbyCreateRequest.Annotation[] + { + }, + capacity = capacity, + is_joinable = true, + name = lobbyName, + tags = new string[] + { + } + }); + } + + public void SetServerLobbyParams(LobbyCreateRequest request) + { + _playerId = request.player.id; + _request = request; + } + + private void OnDestroy() + { + // attempt to clean up lobbies, if active + if (NetworkServer.active) + { + ServerStop(); + // Absolutely make sure there's time for the network request to hit edgegap servers. + // sorry. this can go once the lobby service can timeout lobbies itself + Thread.Sleep(300); + } + else if (NetworkClient.active) + { + ClientDisconnect(); + // Absolutely make sure there's time for the network request to hit edgegap servers. + // sorry. this can go once the lobby service can timeout lobbies itself + Thread.Sleep(300); + } + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta new file mode 100644 index 000000000..1758893ee --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa9d4c3f48a245ed89f122f44e1e81ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs new file mode 100644 index 000000000..c73a7075f --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace Edgegap +{ + // Implements the edgegap lobby api: https://docs.edgegap.com/docs/lobby/functions + public class LobbyApi + { + [Header("Lobby Config")] + public string LobbyUrl; + public LobbyBrief[] Lobbies; + + public LobbyApi(string url) + { + LobbyUrl = url; + } + + + + private static UnityWebRequest SendJson(string url, T data, string method = "POST") + { + string body = JsonUtility.ToJson(data); + UnityWebRequest request = new UnityWebRequest(url, method); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Accept", "application/json"); + request.SetRequestHeader("Content-Type", "application/json"); + return request; + } + + private static bool CheckErrorResponse(UnityWebRequest request, Action onError) + { +#if UNITY_2020_3_OR_NEWER + if (request.result != UnityWebRequest.Result.Success) + { + // how I hate http libs that think they need to be smart and handle status code errors. + if (request.result != UnityWebRequest.Result.ProtocolError || request.responseCode == 0) + { + onError?.Invoke(request.error); + return true; + } + } +#else + if (request.isNetworkError) + { + onError?.Invoke(request.error); + return true; + } +#endif + if (request.responseCode < 200 || request.responseCode >= 300) + { + onError?.Invoke($"non-200 status code: {request.responseCode}. Body:\n {request.downloadHandler.text}"); + return true; + } + return false; + } + + public void RefreshLobbies(Action onLoaded, Action onError) + { + UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies"); + request.SendWebRequest().completed += operation => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + ListLobbiesResponse lobbies = JsonUtility.FromJson(request.downloadHandler.text); + Lobbies = lobbies.data; + onLoaded?.Invoke(lobbies.data); + } + }; + } + + public void CreateLobby(LobbyCreateRequest createData, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies", createData); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + Lobby lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void UpdateLobby(string lobbyId, LobbyUpdateRequest updateData, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", updateData, "PATCH"); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyBrief lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void GetLobby(string lobbyId, Action onResponse, Action onError) + { + UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies/{lobbyId}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + Lobby lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void JoinLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:join", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void LeaveLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:leave", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void StartLobby(LobbyIdRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:start", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void DeleteLobby(string lobbyId, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", "", "DELETE"); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + struct CreateLobbyServiceRequest + { + public string name; + } + public struct LobbyServiceResponse + { + public string name; + public string url; + public string status; + } + + public static void TrimApiKey(ref string apiKey) + { + if (apiKey == null) + { + return; + } + if (apiKey.StartsWith("token ")) + { + apiKey = apiKey.Substring("token ".Length); + } + apiKey = apiKey.Trim(); + } + + public static void CreateAndDeployLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + // try to get the lobby first + GetLobbyService(apiKey, name, response => + { + if (response == null) + { + CreateLobbyService(apiKey, name, onResponse, onError); + } + else if (!string.IsNullOrEmpty(response.Value.url)) + { + onResponse(response.Value); + } + else + { + DeployLobbyService(apiKey, name, onResponse, onError); + } + }, onError); + } + + private static void CreateLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson("https://api.edgegap.com/v1/lobbies", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + DeployLobbyService(apiKey, name, onResponse, onError); + } + }; + } + + public static void GetLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + var request = UnityWebRequest.Get($"https://api.edgegap.com/v1/lobbies/{name}"); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (request.responseCode == 404) + { + onResponse(null); + return; + } + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse(response); + } + }; + } + + public static void TerminateLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + var request = SendJson("https://api.edgegap.com/v1/lobbies:terminate", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(response); + } + }; + } + private static void DeployLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + var request = SendJson("https://api.edgegap.com/v1/lobbies:deploy", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(response); + } + }; + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta new file mode 100644 index 000000000..5c39381c2 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64510fc75d0d75f4185fec1cf4d12206 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs new file mode 100644 index 000000000..7aee63c09 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading; +using UnityEditor; +using UnityEngine; +#if UNITY_EDITOR +namespace Edgegap +{ + public class LobbyServiceCreateDialogue : EditorWindow + { + public Action onLobby; + public bool waitingCreate; + public bool waitingStatus; + private string _name; + private string _key; + private string _lastStatus; + + private void Awake() + { + minSize = maxSize = new Vector2(450, 300); + titleContent = new GUIContent("Edgegap Lobby Service Setup"); + } + + private void OnGUI() + { + if (waitingCreate) + { + EditorGUILayout.LabelField("Waiting for lobby to create . . . "); + return; + } + if (waitingStatus) + { + EditorGUILayout.LabelField("Waiting for lobby to deploy . . . "); + EditorGUILayout.LabelField($"Latest status: {_lastStatus}"); + return; + } + _key = EditorGUILayout.TextField("Edgegap API key", _key); + LobbyApi.TrimApiKey(ref _key); + EditorGUILayout.HelpBox(new GUIContent("Your API key won't be saved.")); + if (GUILayout.Button("I have no api key?")) + { + Application.OpenURL("https://app.edgegap.com/user-settings?tab=tokens"); + } + EditorGUILayout.Separator(); + EditorGUILayout.HelpBox("There's currently a bug where lobby names longer than 5 characters can fail to deploy correctly and will return a \"503 Service Temporarily Unavailable\"\nIt's recommended to limit your lobby names to 4-5 characters for now", UnityEditor.MessageType.Warning); + _name = EditorGUILayout.TextField("Lobby Name", _name); + EditorGUILayout.HelpBox(new GUIContent("The lobby name is your games identifier for the lobby service")); + + if (GUILayout.Button("Create")) + { + if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name)) + { + EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok"); + } + else + { + waitingCreate = true; + Repaint(); + + LobbyApi.CreateAndDeployLobbyService(_key.Trim(), _name.Trim(), res => + { + waitingCreate = false; + waitingStatus = true; + _lastStatus = res.status; + RefreshStatus(); + Repaint(); + }, error => + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) the lobby service:\n\n{error}", "Ok"); + waitingCreate = false; + }); + return; + } + + } + + if (GUILayout.Button("Cancel")) + Close(); + + EditorGUILayout.HelpBox(new GUIContent("Note: If you forgot your lobby url simply re-create it with the same name!\nIt will re-use the existing lobby service")); + EditorGUILayout.Separator(); + EditorGUILayout.Separator(); + + + if (GUILayout.Button("Terminate existing deploy")) + { + + if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name)) + { + EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok"); + } + else + { + LobbyApi.TerminateLobbyService(_key.Trim(), _name.Trim(), res => + { + EditorUtility.DisplayDialog("Success", $"The lobby service will start terminating (shutting down the deploy) now", "Ok"); + }, error => + { + EditorUtility.DisplayDialog("Failed to terminate lobby", $"The following error happened while trying to terminate the lobby service:\n\n{error}", "Ok"); + }); + } + } + EditorGUILayout.HelpBox(new GUIContent("Done with your lobby?\nEnter the same name as creation to shut it down")); + } + private void RefreshStatus() + { + // Stop if window is closed + if (!this) + { + return; + } + LobbyApi.GetLobbyService(_key, _name, res => + { + if (!res.HasValue) + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The lobby seems to have vanished while waiting for it to deploy.", "Ok"); + waitingStatus = false; + Repaint(); + return; + } + if (!string.IsNullOrEmpty(res.Value.url)) + { + onLobby(res.Value.url); + Close(); + return; + } + _lastStatus = res.Value.status; + Repaint(); + Thread.Sleep(100); // :( but this is a lazy editor script, its fiiine + RefreshStatus(); + }, error => + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) a lobby:\n\n{error}", "Ok"); + waitingStatus = false; + }); + } + } +} +#endif diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta new file mode 100644 index 000000000..0a52bdd9e --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25579cc004424981bf0b05bcec65df0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs new file mode 100644 index 000000000..3d9aa32d4 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Reflection; +using kcp2k; +using UnityEditor; +using UnityEngine; +#if UNITY_EDITOR +namespace Edgegap +{ + [CustomEditor(typeof(EdgegapLobbyKcpTransport))] + public class EncryptionTransportInspector : UnityEditor.Editor + { + SerializedProperty lobbyUrlProperty; + SerializedProperty lobbyWaitTimeoutProperty; + private List kcpProperties = new List(); + + + // Assuming proper SerializedProperty definitions for properties + // Add more SerializedProperty fields related to different modes as needed + + void OnEnable() + { + lobbyUrlProperty = serializedObject.FindProperty("lobbyUrl"); + lobbyWaitTimeoutProperty = serializedObject.FindProperty("lobbyWaitTimeout"); + // Get public fields from KcpTransport + kcpProperties.Clear(); + FieldInfo[] fields = typeof(KcpTransport).GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var field in fields) + { + SerializedProperty prop = serializedObject.FindProperty(field.Name); + if (prop == null) + { + // callbacks have no property + continue; + } + kcpProperties.Add(prop); + } + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.PropertyField(lobbyUrlProperty); + if (GUILayout.Button("Create & Deploy Lobby")) + { + var input = CreateInstance(); + input.onLobby = (url) => + { + lobbyUrlProperty.stringValue = url; + serializedObject.ApplyModifiedProperties(); + }; + input.ShowUtility(); + } + EditorGUILayout.PropertyField(lobbyWaitTimeoutProperty); + EditorGUILayout.Separator(); + foreach (SerializedProperty prop in kcpProperties) + { + EditorGUILayout.PropertyField(prop); + } + serializedObject.ApplyModifiedProperties(); + } + } +} + +#endif diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta new file mode 100644 index 000000000..d0bf96ed5 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d7cc53263184754a4682335440df515 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta new file mode 100644 index 000000000..e3ab91eec --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9b459cf5e084bdd8b196df849a2c519 +timeCreated: 1709953502 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs new file mode 100644 index 000000000..dd8ac684c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#functions + [Serializable] + public struct ListLobbiesResponse + { + public int count; + public LobbyBrief[] data; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta new file mode 100644 index 000000000..b07e4fab2 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fdb37041d9464f8c90ac86942b940565 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs new file mode 100644 index 000000000..c08c899c2 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs @@ -0,0 +1,45 @@ +using System; +using UnityEngine; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#getting-a-specific-lobbys-information + [Serializable] + public struct Lobby + { + [Serializable] + public struct Player + { + public uint authorization_token; + public string id; + public bool is_host; + } + + [Serializable] + public struct Port + { + public string name; + public int port; + public string protocol; + } + + [Serializable] + public struct Assignment + { + public uint authorization_token; + public string host; + public string ip; + public Port[] ports; + } + + public Assignment assignment; + public string name; + public string lobby_id; + public bool is_joinable; + public bool is_started; + public int player_count; + public int capacity; + public int available_slots => capacity - player_count; + public string[] tags; + public Player[] players; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta new file mode 100644 index 000000000..d2d7b631b --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64db55f096cd4ace83e1aa1c0c0588f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs new file mode 100644 index 000000000..1e927e3a4 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs @@ -0,0 +1,17 @@ +using System; +namespace Edgegap +{ + // Brief lobby data, returned by the list function + [Serializable] + public struct LobbyBrief + { + public string lobby_id; + public string name; + public bool is_joinable; + public bool is_started; + public int player_count; + public int capacity; + public int available_slots => capacity - player_count; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta new file mode 100644 index 000000000..408c87b55 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6018ece006144e719c6b3f0d4e256d7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs new file mode 100644 index 000000000..2ed819dfa --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs @@ -0,0 +1,27 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#creating-a-new-lobby + [Serializable] + public struct LobbyCreateRequest + { + [Serializable] + public struct Player + { + public string id; + } + [Serializable] + public struct Annotation + { + public bool inject; + public string key; + public string value; + } + public Annotation[] annotations; // todo + public int capacity; + public bool is_joinable; + public string name; + public Player player; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta new file mode 100644 index 000000000..1f0bb18b3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4040c1adafc3449eaebd3bd22aa3ff26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs new file mode 100644 index 000000000..3647979c0 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs @@ -0,0 +1,14 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions/#starting-a-lobby + [Serializable] + public struct LobbyIdRequest + { + public string lobby_id; + public LobbyIdRequest(string lobbyId) + { + lobby_id = lobbyId; + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta new file mode 100644 index 000000000..f77f664e1 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 219c7fba8724473caf170c6254e6dc45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs new file mode 100644 index 000000000..7c2115fbe --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs @@ -0,0 +1,17 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby + // https://docs.edgegap.com/docs/lobby/functions#leaving-a-lobby + [Serializable] + public struct LobbyJoinOrLeaveRequest + { + [Serializable] + public struct Player + { + public string id; + } + public string lobby_id; + public Player player; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta new file mode 100644 index 000000000..8861be513 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4091d555e62341f0ac30479952d517aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs new file mode 100644 index 000000000..3b8b53c17 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs @@ -0,0 +1,12 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby + [Serializable] + public struct LobbyUpdateRequest + { + public int capacity; + public bool is_joinable; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta new file mode 100644 index 000000000..17e9ac702 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee158bc379f44cdf9904578f37a5e7a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs index 5c057b0b0..a7e2ff352 100644 --- a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs @@ -10,7 +10,7 @@ namespace Edgegap { - [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")] public class EdgegapKcpTransport : KcpTransport { [Header("Relay")] @@ -53,7 +53,7 @@ protected override void Awake() client = new EdgegapKcpClient( () => OnClientConnected.Invoke(), (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), - () => OnClientDisconnected.Invoke(), + () => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708 (error, reason) => OnClientError.Invoke(ToTransportError(error), reason), config ); diff --git a/Assets/Mirror/Transports/Encryption.meta b/Assets/Mirror/Transports/Encryption.meta new file mode 100644 index 000000000..6c507ab49 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 741b3c7e5d0842049ff50a2f6e27ca12 +timeCreated: 1708015148 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/Editor.meta b/Assets/Mirror/Transports/Encryption/Editor.meta new file mode 100644 index 000000000..b6cf690a7 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0d3cd9d7d6e84a578f7e4b384ff813f1 +timeCreated: 1708793986 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef new file mode 100644 index 000000000..0ba9c7627 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "EncryptionTransportEditor", + "rootNamespace": "", + "references": [ + "GUID:627104647b9c04b4ebb8978a92ecac63" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta new file mode 100644 index 000000000..43ba20c69 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4c9c7b0ef83e6e945b276d644816a489 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs new file mode 100644 index 000000000..24557f910 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs @@ -0,0 +1,81 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror.Transports.Encryption +{ + [CustomEditor(typeof(EncryptionTransport), true)] + public class EncryptionTransportInspector : UnityEditor.Editor + { + SerializedProperty innerProperty; + SerializedProperty clientValidatesServerPubKeyProperty; + SerializedProperty clientTrustedPubKeySignaturesProperty; + SerializedProperty serverKeypairPathProperty; + SerializedProperty serverLoadKeyPairFromFileProperty; + + // Assuming proper SerializedProperty definitions for properties + // Add more SerializedProperty fields related to different modes as needed + + void OnEnable() + { + innerProperty = serializedObject.FindProperty("inner"); + clientValidatesServerPubKeyProperty = serializedObject.FindProperty("clientValidateServerPubKey"); + clientTrustedPubKeySignaturesProperty = serializedObject.FindProperty("clientTrustedPubKeySignatures"); + serverKeypairPathProperty = serializedObject.FindProperty("serverKeypairPath"); + serverLoadKeyPairFromFileProperty = serializedObject.FindProperty("serverLoadKeyPairFromFile"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField("Common", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(innerProperty); + EditorGUILayout.Separator(); + // Client Section + EditorGUILayout.LabelField("Client", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Validating the servers public key is essential for complete man-in-the-middle (MITM) safety, but might not be feasible for all modes of hosting.", MessageType.Info); + EditorGUILayout.PropertyField(clientValidatesServerPubKeyProperty, new GUIContent("Validate Server Public Key")); + + EncryptionTransport.ValidationMode validationMode = (EncryptionTransport.ValidationMode)clientValidatesServerPubKeyProperty.enumValueIndex; + + switch (validationMode) + { + case EncryptionTransport.ValidationMode.List: + EditorGUILayout.PropertyField(clientTrustedPubKeySignaturesProperty); + break; + case EncryptionTransport.ValidationMode.Callback: + EditorGUILayout.HelpBox("Please set the EncryptionTransport.onClientValidateServerPubKey at runtime.", MessageType.Info); + break; + } + + EditorGUILayout.Separator(); + // Server Section + EditorGUILayout.LabelField("Server", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(serverLoadKeyPairFromFileProperty, new GUIContent("Load Keypair From File")); + if (serverLoadKeyPairFromFileProperty.boolValue) + { + EditorGUILayout.PropertyField(serverKeypairPathProperty, new GUIContent("Keypair File Path")); + } + if(GUILayout.Button("Generate Key Pair")) + { + EncryptionCredentials keyPair = EncryptionCredentials.Generate(); + string path = EditorUtility.SaveFilePanel("Select where to save the keypair", "", "server-keys.json", "json"); + if (!string.IsNullOrEmpty(path)) + { + keyPair.SaveToFile(path); + EditorUtility.DisplayDialog("KeyPair Saved", $"Successfully saved the keypair.\nThe fingerprint is {keyPair.PublicKeyFingerprint}, you can also retrieve it from the saved json file at any point.", "Ok"); + if (validationMode == EncryptionTransport.ValidationMode.List) + { + if (EditorUtility.DisplayDialog("Add key to trusted list?", "Do you also want to add the generated key to the trusted list?", "Yes", "No")) + { + clientTrustedPubKeySignaturesProperty.arraySize++; + clientTrustedPubKeySignaturesProperty.GetArrayElementAtIndex(clientTrustedPubKeySignaturesProperty.arraySize - 1).stringValue = keyPair.PublicKeyFingerprint; + } + } + } + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta new file mode 100644 index 000000000..9aad40bb3 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 871580d2094a46139279d651cec92b5d +timeCreated: 1708794004 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs new file mode 100644 index 000000000..9515e74db --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs @@ -0,0 +1,595 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Agreement; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using UnityEngine.Profiling; + +namespace Mirror.Transports.Encryption +{ + public class EncryptedConnection + { + // 256-bit key + private const int KeyLength = 32; + // 512-bit salt for the key derivation function + private const int HkdfSaltSize = KeyLength * 2; + + // Info tag for the HKDF, this just adds more entropy + private static readonly byte[] HkdfInfo = Encoding.UTF8.GetBytes("Mirror/EncryptionTransport"); + + // fixed size of the unique per-packet nonce. Defaults to 12 bytes/96 bits (not recommended to be changed) + private const int NonceSize = 12; + + // this is the size of the "checksum" included in each encrypted payload + // 16 bytes/128 bytes is the recommended value for best security + // can be reduced to 12 bytes for a small space savings, but makes encryption slightly weaker. + // Setting it lower than 12 bytes is not recommended + private const int MacSizeBytes = 16; + + private const int MacSizeBits = MacSizeBytes * 8; + + // How much metadata overhead we have for regular packets + public const int Overhead = sizeof(OpCodes) + MacSizeBytes + NonceSize; + + // After how many seconds of not receiving a handshake packet we should time out + private const double DurationTimeout = 2; // 2s + + // After how many seconds to assume the last handshake packet got lost and to resend another one + private const double DurationResend = 0.05; // 50ms + + + // Static fields for allocation efficiency, makes this not thread safe + // It'd be as easy as using ThreadLocal though to fix that + + // Set up a global cipher instance, it is initialised/reset before use + // (AesFastEngine used to exist, but was removed due to side channel issues) + // use AesUtilities.CreateEngine here as it'll pick the hardware accelerated one if available (which is will not be unless on .net core) + private static readonly GcmBlockCipher Cipher = new GcmBlockCipher(AesUtilities.CreateEngine()); + + // Set up a global HKDF with a SHA-256 digest + private static readonly HkdfBytesGenerator Hkdf = new HkdfBytesGenerator(new Sha256Digest()); + + // Global byte array to store nonce sent by the remote side, they're used immediately after + private static readonly byte[] ReceiveNonce = new byte[NonceSize]; + + // Buffer for the remote salt, as bouncycastle needs to take a byte[] *rolls eyes* + private static byte[] _tmpRemoteSaltBuffer = new byte[HkdfSaltSize]; + // buffer for encrypt/decrypt operations, resized larger as needed + // this is also the buffer that will be returned to mirror via ArraySegment + // so any thread safety concerns would need to take extra care here + private static byte[] _tmpCryptBuffer = new byte[2048]; + + // packet headers + enum OpCodes : byte + { + // start at 1 to maybe filter out random noise + Data = 1, + HandshakeStart = 2, + HandshakeAck = 3, + HandshakeFin = 4, + } + + enum State + { + // Waiting for a handshake to arrive + // this is for _sendsFirst: + // - false: OpCodes.HandshakeStart + // - true: Opcodes.HandshakeAck + WaitingHandshake, + + // Waiting for a handshake reply/acknowledgement to arrive + // this is for _sendsFirst: + // - false: OpCodes.HandshakeFine + // - true: Opcodes.Data (implicitly) + WaitingHandshakeReply, + + // Both sides have confirmed the keys are exchanged and data can be sent freely + Ready + } + + private State _state = State.WaitingHandshake; + + // Key exchange confirmed and data can be sent freely + public bool IsReady => _state == State.Ready; + // Callback to send off encrypted data + private Action, int> _send; + // Callback when received data has been decrypted + private Action, int> _receive; + // Callback when the connection becomes ready + private Action _ready; + // On-error callback, disconnect expected + private Action _error; + // Optional callback to validate the remotes public key, validation on one side is necessary to ensure MITM resistance + // (usually client validates the server key) + private Func _validateRemoteKey; + // Our asymmetric credentials for the initial DH exchange + private EncryptionCredentials _credentials; + private byte[] _hkdfSalt; + + // After no handshake packet in this many seconds, the handshake fails + private double _handshakeTimeout; + // When to assume the last handshake packet got lost and to resend another one + private double _nextHandshakeResend; + + + // we can reuse the _cipherParameters here since the nonce is stored as the byte[] reference we pass in + // so we can update it without creating a new AeadParameters instance + // this might break in the future! (will cause bad data) + private byte[] _nonce = new byte[NonceSize]; + private AeadParameters _cipherParametersEncrypt; + private AeadParameters _cipherParametersDecrypt; + + + /* + * Specifies if we send the first key, then receive ack, then send fin + * Or the opposite if set to false + * + * The client does this, since the fin is not acked explicitly, but by receiving data to decrypt + */ + private readonly bool _sendsFirst; + + public EncryptedConnection(EncryptionCredentials credentials, + bool isClient, + Action, int> sendAction, + Action, int> receiveAction, + Action readyAction, + Action errorAction, + Func validateRemoteKey = null) + { + _credentials = credentials; + _sendsFirst = isClient; + if (!_sendsFirst) + { + // salt is controlled by the server + _hkdfSalt = GenerateSecureBytes(HkdfSaltSize); + } + _send = sendAction; + _receive = receiveAction; + _ready = readyAction; + _error = errorAction; + _validateRemoteKey = validateRemoteKey; + } + + // Generates a random starting nonce + private static byte[] GenerateSecureBytes(int size) + { + byte[] bytes = new byte[size]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(bytes); + } + + return bytes; + } + + public void OnReceiveRaw(ArraySegment data, int channel) + { + if (data.Count < 1) + { + _error(TransportError.Unexpected, "Received empty packet"); + return; + } + + using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) + { + OpCodes opcode = (OpCodes)reader.ReadByte(); + switch (opcode) + { + case OpCodes.Data: + // first sender ready is implicit when data is received + if (_sendsFirst && _state == State.WaitingHandshakeReply) + { + SetReady(); + } + else if (!IsReady) + { + _error(TransportError.Unexpected, "Unexpected data while not ready."); + } + + if (reader.Remaining < Overhead) + { + _error(TransportError.Unexpected, "received data packet smaller than metadata size"); + return; + } + + ArraySegment ciphertext = reader.ReadBytesSegment(reader.Remaining - NonceSize); + reader.ReadBytes(ReceiveNonce, NonceSize); + + Profiler.BeginSample("EncryptedConnection.Decrypt"); + ArraySegment plaintext = Decrypt(ciphertext); + Profiler.EndSample(); + if (plaintext.Count == 0) + { + // error + return; + } + _receive(plaintext, channel); + break; + case OpCodes.HandshakeStart: + if (_sendsFirst) + { + _error(TransportError.Unexpected, "Received HandshakeStart packet, we don't expect this."); + return; + } + + if (_state == State.WaitingHandshakeReply) + { + // this is fine, packets may arrive out of order + return; + } + + _state = State.WaitingHandshakeReply; + ResetTimeouts(); + CompleteExchange(reader.ReadBytesSegment(reader.Remaining), _hkdfSalt); + SendHandshakeAndPubKey(OpCodes.HandshakeAck); + break; + case OpCodes.HandshakeAck: + if (!_sendsFirst) + { + _error(TransportError.Unexpected, "Received HandshakeAck packet, we don't expect this."); + return; + } + + if (IsReady) + { + // this is fine, packets may arrive out of order + return; + } + + if (_state == State.WaitingHandshakeReply) + { + // this is fine, packets may arrive out of order + return; + } + + + _state = State.WaitingHandshakeReply; + ResetTimeouts(); + reader.ReadBytes(_tmpRemoteSaltBuffer, HkdfSaltSize); + CompleteExchange(reader.ReadBytesSegment(reader.Remaining), _tmpRemoteSaltBuffer); + SendHandshakeFin(); + break; + case OpCodes.HandshakeFin: + if (_sendsFirst) + { + _error(TransportError.Unexpected, "Received HandshakeFin packet, we don't expect this."); + return; + } + + if (IsReady) + { + // this is fine, packets may arrive out of order + return; + } + + if (_state != State.WaitingHandshakeReply) + { + _error(TransportError.Unexpected, + "Received HandshakeFin packet, we didn't expect this yet."); + return; + } + + SetReady(); + + break; + default: + _error(TransportError.InvalidReceive, $"Unhandled opcode {(byte)opcode:x}"); + break; + } + } + } + private void SetReady() + { + // done with credentials, null out the reference + _credentials = null; + + _state = State.Ready; + _ready(); + } + + private void ResetTimeouts() + { + _handshakeTimeout = 0; + _nextHandshakeResend = -1; + } + + public void Send(ArraySegment data, int channel) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteByte((byte)OpCodes.Data); + Profiler.BeginSample("EncryptedConnection.Encrypt"); + ArraySegment encrypted = Encrypt(data); + Profiler.EndSample(); + + if (encrypted.Count == 0) + { + // error + return; + } + writer.WriteBytes(encrypted.Array, 0, encrypted.Count); + // write nonce after since Encrypt will update it + writer.WriteBytes(_nonce, 0, NonceSize); + _send(writer.ToArraySegment(), channel); + } + } + + private ArraySegment Encrypt(ArraySegment plaintext) + { + if (plaintext.Count == 0) + { + // Invalid + return new ArraySegment(); + } + // Need to make the nonce unique again before encrypting another message + UpdateNonce(); + // Re-initialize the cipher with our cached parameters + Cipher.Init(true, _cipherParametersEncrypt); + + // Calculate the expected output size, this should always be input size + mac size + int outSize = Cipher.GetOutputSize(plaintext.Count); +#if UNITY_EDITOR + // expecting the outSize to be input size + MacSize + if (outSize != plaintext.Count + MacSizeBytes) + { + throw new Exception($"Encrypt: Unexpected output size (Expected {plaintext.Count + MacSizeBytes}, got {outSize}"); + } +#endif + // Resize the static buffer to fit + EnsureSize(ref _tmpCryptBuffer, outSize); + int resultLen; + try + { + // Run the plain text through the cipher, ProcessBytes will only process full blocks + resultLen = + Cipher.ProcessBytes(plaintext.Array, plaintext.Offset, plaintext.Count, _tmpCryptBuffer, 0); + // Then run any potentially remaining partial blocks through with DoFinal (and calculate the mac) + resultLen += Cipher.DoFinal(_tmpCryptBuffer, resultLen); + } + // catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types + // + catch (Exception e) + { + _error(TransportError.Unexpected, $"Unexpected exception while encrypting {e.GetType()}: {e.Message}"); + return new ArraySegment(); + } +#if UNITY_EDITOR + // expecting the result length to match the previously calculated input size + MacSize + if (resultLen != outSize) + { + throw new Exception($"Encrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})"); + } +#endif + return new ArraySegment(_tmpCryptBuffer, 0, resultLen); + } + + private ArraySegment Decrypt(ArraySegment ciphertext) + { + if (ciphertext.Count <= MacSizeBytes) + { + _error(TransportError.Unexpected, $"Received too short data packet (min {{MacSizeBytes + 1}}, got {ciphertext.Count})"); + // Invalid + return new ArraySegment(); + } + // Re-initialize the cipher with our cached parameters + Cipher.Init(false, _cipherParametersDecrypt); + + // Calculate the expected output size, this should always be input size - mac size + int outSize = Cipher.GetOutputSize(ciphertext.Count); +#if UNITY_EDITOR + // expecting the outSize to be input size - MacSize + if (outSize != ciphertext.Count - MacSizeBytes) + { + throw new Exception($"Decrypt: Unexpected output size (Expected {ciphertext.Count - MacSizeBytes}, got {outSize}"); + } +#endif + // Resize the static buffer to fit + EnsureSize(ref _tmpCryptBuffer, outSize); + int resultLen; + try + { + // Run the ciphertext through the cipher, ProcessBytes will only process full blocks + resultLen = + Cipher.ProcessBytes(ciphertext.Array, ciphertext.Offset, ciphertext.Count, _tmpCryptBuffer, 0); + // Then run any potentially remaining partial blocks through with DoFinal (and calculate/check the mac) + resultLen += Cipher.DoFinal(_tmpCryptBuffer, resultLen); + } + // catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types + catch (Exception e) + { + _error(TransportError.Unexpected, $"Unexpected exception while decrypting {e.GetType()}: {e.Message}. This usually signifies corrupt data"); + return new ArraySegment(); + } +#if UNITY_EDITOR + // expecting the result length to match the previously calculated input size + MacSize + if (resultLen != outSize) + { + throw new Exception($"Decrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})"); + } +#endif + return new ArraySegment(_tmpCryptBuffer, 0, resultLen); + } + + private void UpdateNonce() + { + // increment the nonce by one + // we need to ensure the nonce is *always* unique and not reused + // easiest way to do this is by simply incrementing it + for (int i = 0; i < NonceSize; i++) + { + _nonce[i]++; + if (_nonce[i] != 0) + { + break; + } + } + } + + private static void EnsureSize(ref byte[] buffer, int size) + { + if (buffer.Length < size) + { + // double buffer to avoid constantly resizing by a few bytes + Array.Resize(ref buffer, Math.Max(size, buffer.Length * 2)); + } + } + + private void SendHandshakeAndPubKey(OpCodes opcode) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteByte((byte)opcode); + if (opcode == OpCodes.HandshakeAck) + { + writer.WriteBytes(_hkdfSalt, 0, HkdfSaltSize); + } + writer.WriteBytes(_credentials.PublicKeySerialized, 0, _credentials.PublicKeySerialized.Length); + _send(writer.ToArraySegment(), Channels.Unreliable); + } + } + + private void SendHandshakeFin() + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteByte((byte)OpCodes.HandshakeFin); + _send(writer.ToArraySegment(), Channels.Unreliable); + } + } + + private void CompleteExchange(ArraySegment remotePubKeyRaw, byte[] salt) + { + AsymmetricKeyParameter remotePubKey; + try + { + remotePubKey = EncryptionCredentials.DeserializePublicKey(remotePubKeyRaw); + } + catch (Exception e) + { + _error(TransportError.Unexpected, $"Failed to deserialize public key of remote. {e.GetType()}: {e.Message}"); + return; + } + + if (_validateRemoteKey != null) + { + PubKeyInfo info = new PubKeyInfo + { + Fingerprint = EncryptionCredentials.PubKeyFingerprint(remotePubKeyRaw), + Serialized = remotePubKeyRaw, + Key = remotePubKey + }; + if (!_validateRemoteKey(info)) + { + _error(TransportError.Unexpected, $"Remote public key (fingerprint: {info.Fingerprint}) failed validation. "); + return; + } + } + + // Calculate a common symmetric key from our private key and the remotes public key + // This gives us the same key on the other side, with our public key and their remote + // It's like magic, but with math! + ECDHBasicAgreement ecdh = new ECDHBasicAgreement(); + ecdh.Init(_credentials.PrivateKey); + byte[] sharedSecret; + try + { + sharedSecret = ecdh.CalculateAgreement(remotePubKey).ToByteArrayUnsigned(); + } + catch + (Exception e) + { + _error(TransportError.Unexpected, $"Failed to calculate the ECDH key exchange. {e.GetType()}: {e.Message}"); + return; + } + + if (salt.Length != HkdfSaltSize) + { + _error(TransportError.Unexpected, $"Salt is expected to be {HkdfSaltSize} bytes long, got {salt.Length}."); + return; + } + + Hkdf.Init(new HkdfParameters(sharedSecret, salt, HkdfInfo)); + + // Allocate a buffer for the output key + byte[] keyRaw = new byte[KeyLength]; + + // Generate the output keying material + Hkdf.GenerateBytes(keyRaw, 0, keyRaw.Length); + + KeyParameter key = new KeyParameter(keyRaw); + + // generate a starting nonce + _nonce = GenerateSecureBytes(NonceSize); + + // we pass in the nonce array once (as it's stored by reference) so we can cache the AeadParameters instance + // instead of creating a new one each encrypt/decrypt + _cipherParametersEncrypt = new AeadParameters(key, MacSizeBits, _nonce); + _cipherParametersDecrypt = new AeadParameters(key, MacSizeBits, ReceiveNonce); + } + + /** + * non-ready connections need to be ticked for resending key data over unreliable + */ + public void TickNonReady(double time) + { + if (IsReady) + { + return; + } + + // Timeout reset + if (_handshakeTimeout == 0) + { + _handshakeTimeout = time + DurationTimeout; + } + else if (time > _handshakeTimeout) + { + _error?.Invoke(TransportError.Timeout, $"Timed out during {_state}, this probably just means the other side went away which is fine."); + return; + } + + // Timeout reset + if (_nextHandshakeResend < 0) + { + _nextHandshakeResend = time + DurationResend; + return; + } + + if (time < _nextHandshakeResend) + { + // Resend isn't due yet + return; + } + + _nextHandshakeResend = time + DurationResend; + switch (_state) + { + case State.WaitingHandshake: + if (_sendsFirst) + { + SendHandshakeAndPubKey(OpCodes.HandshakeStart); + } + + break; + case State.WaitingHandshakeReply: + if (_sendsFirst) + { + SendHandshakeFin(); + } + else + { + SendHandshakeAndPubKey(OpCodes.HandshakeAck); + } + + break; + case State.Ready: // IsReady is checked above & early-returned + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta new file mode 100644 index 000000000..b5e52091b --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 28f3ac4ff1d346a895d0b4ff714fb57b +timeCreated: 1708111337 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs new file mode 100644 index 000000000..2e0b04259 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs @@ -0,0 +1,125 @@ +using System; +using System.IO; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using UnityEngine; + +namespace Mirror.Transports.Encryption +{ + public class EncryptionCredentials + { + const int PrivateKeyBits = 256; + // don't actually need to store this currently + // but we'll need to for loading/saving from file maybe? + // public ECPublicKeyParameters PublicKey; + + // The serialized public key, in DER format + public byte[] PublicKeySerialized; + public ECPrivateKeyParameters PrivateKey; + public string PublicKeyFingerprint; + + EncryptionCredentials() {} + + // TODO: load from file + public static EncryptionCredentials Generate() + { + var generator = new ECKeyPairGenerator(); + generator.Init(new KeyGenerationParameters(new SecureRandom(), PrivateKeyBits)); + AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair(); + var serialized = SerializePublicKey((ECPublicKeyParameters)keyPair.Public); + return new EncryptionCredentials + { + // see fields above + // PublicKey = (ECPublicKeyParameters)keyPair.Public, + PublicKeySerialized = serialized, + PublicKeyFingerprint = PubKeyFingerprint(new ArraySegment(serialized)), + PrivateKey = (ECPrivateKeyParameters)keyPair.Private + }; + } + + public static byte[] SerializePublicKey(AsymmetricKeyParameter publicKey) + { + // apparently the best way to transmit this public key over the network is to serialize it as a DER + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey); + return publicKeyInfo.ToAsn1Object().GetDerEncoded(); + } + + public static AsymmetricKeyParameter DeserializePublicKey(ArraySegment pubKey) + { + // And then we do this to deserialize from the DER (from above) + // the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted + // to a byte[] first and then shoved through a MemoryStream + return PublicKeyFactory.CreateKey(new MemoryStream(pubKey.Array, pubKey.Offset, pubKey.Count, false)); + } + + public static byte[] SerializePrivateKey(AsymmetricKeyParameter privateKey) + { + // Serialize privateKey as a DER + PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey); + return privateKeyInfo.ToAsn1Object().GetDerEncoded(); + } + + public static AsymmetricKeyParameter DeserializePrivateKey(ArraySegment privateKey) + { + // And then we do this to deserialize from the DER (from above) + // the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted + // to a byte[] first and then shoved through a MemoryStream + return PrivateKeyFactory.CreateKey(new MemoryStream(privateKey.Array, privateKey.Offset, privateKey.Count, false)); + } + + public static string PubKeyFingerprint(ArraySegment publicKeyBytes) + { + Sha256Digest digest = new Sha256Digest(); + byte[] hash = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(publicKeyBytes.Array, publicKeyBytes.Offset, publicKeyBytes.Count); + digest.DoFinal(hash, 0); + + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + public void SaveToFile(string path) + { + string json = JsonUtility.ToJson(new SerializedPair + { + PublicKeyFingerprint = PublicKeyFingerprint, + PublicKey = Convert.ToBase64String(PublicKeySerialized), + PrivateKey= Convert.ToBase64String(SerializePrivateKey(PrivateKey)), + }); + File.WriteAllText(path, json); + } + + public static EncryptionCredentials LoadFromFile(string path) + { + string json = File.ReadAllText(path); + SerializedPair serializedPair = JsonUtility.FromJson(json); + + byte[] publicKeyBytes = Convert.FromBase64String(serializedPair.PublicKey); + byte[] privateKeyBytes = Convert.FromBase64String(serializedPair.PrivateKey); + + if (serializedPair.PublicKeyFingerprint != PubKeyFingerprint(new ArraySegment(publicKeyBytes))) + { + throw new Exception("Saved public key fingerprint does not match public key."); + } + return new EncryptionCredentials + { + PublicKeySerialized = publicKeyBytes, + PublicKeyFingerprint = serializedPair.PublicKeyFingerprint, + PrivateKey = (ECPrivateKeyParameters) DeserializePrivateKey(new ArraySegment(privateKeyBytes)) + }; + } + + private class SerializedPair + { + public string PublicKeyFingerprint; + public string PublicKey; + public string PrivateKey; + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta new file mode 100644 index 000000000..38f119773 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af6ae5f74f9548588cba5731643fabaf +timeCreated: 1708139579 \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs new file mode 100644 index 000000000..5d9d9bb9a --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Serialization; + +namespace Mirror.Transports.Encryption +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")] + public class EncryptionTransport : Transport + { + public override bool IsEncrypted => true; + public override string EncryptionCipher => "AES256-GCM"; + public Transport inner; + + public enum ValidationMode + { + Off, + List, + Callback, + } + + public ValidationMode clientValidateServerPubKey; + [Tooltip("List of public key fingerprints the client will accept")] + public string[] clientTrustedPubKeySignatures; + public Func onClientValidateServerPubKey; + public bool serverLoadKeyPairFromFile; + public string serverKeypairPath = "./server-keys.json"; + + private EncryptedConnection _client; + + private Dictionary _serverConnections = new Dictionary(); + + private List _serverPendingConnections = + new List(); + + private EncryptionCredentials _credentials; + public string EncryptionPublicKeyFingerprint => _credentials?.PublicKeyFingerprint; + public byte[] EncryptionPublicKey => _credentials?.PublicKeySerialized; + + private void ServerRemoveFromPending(EncryptedConnection con) + { + for (int i = 0; i < _serverPendingConnections.Count; i++) + { + if (_serverPendingConnections[i] == con) + { + // remove by swapping with last + int lastIndex = _serverPendingConnections.Count - 1; + _serverPendingConnections[i] = _serverPendingConnections[lastIndex]; + _serverPendingConnections.RemoveAt(lastIndex); + break; + } + } + } + + private void HandleInnerServerDisconnected(int connId) + { + if (_serverConnections.TryGetValue(connId, out EncryptedConnection con)) + { + ServerRemoveFromPending(con); + _serverConnections.Remove(connId); + } + OnServerDisconnected?.Invoke(connId); + } + + private void HandleInnerServerError(int connId, TransportError type, string msg) + { + OnServerError?.Invoke(connId, type, $"inner: {msg}"); + } + + private void HandleInnerServerDataReceived(int connId, ArraySegment data, int channel) + { + if (_serverConnections.TryGetValue(connId, out EncryptedConnection c)) + { + c.OnReceiveRaw(data, channel); + } + } + + private void HandleInnerServerConnected(int connId) + { + Debug.Log($"[EncryptionTransport] New connection #{connId}"); + EncryptedConnection ec = null; + ec = new EncryptedConnection( + _credentials, + false, + (segment, channel) => inner.ServerSend(connId, segment, channel), + (segment, channel) => OnServerDataReceived?.Invoke(connId, segment, channel), + () => + { + Debug.Log($"[EncryptionTransport] Connection #{connId} is ready"); + ServerRemoveFromPending(ec); + OnServerConnected?.Invoke(connId); + }, + (type, msg) => + { + OnServerError?.Invoke(connId, type, msg); + ServerDisconnect(connId); + }); + _serverConnections.Add(connId, ec); + _serverPendingConnections.Add(ec); + } + + private void HandleInnerClientDisconnected() + { + _client = null; + OnClientDisconnected?.Invoke(); + } + + private void HandleInnerClientError(TransportError arg1, string arg2) + { + OnClientError?.Invoke(arg1, $"inner: {arg2}"); + } + + private void HandleInnerClientDataReceived(ArraySegment data, int channel) + { + _client?.OnReceiveRaw(data, channel); + } + + private void HandleInnerClientConnected() + { + _client = new EncryptedConnection( + _credentials, + true, + (segment, channel) => inner.ClientSend(segment, channel), + (segment, channel) => OnClientDataReceived?.Invoke(segment, channel), + () => + { + OnClientConnected?.Invoke(); + }, + (type, msg) => + { + OnClientError?.Invoke(type, msg); + ClientDisconnect(); + }, + HandleClientValidateServerPubKey); + } + + private bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo) + { + switch (clientValidateServerPubKey) + { + case ValidationMode.Off: + return true; + case ValidationMode.List: + return Array.IndexOf(clientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0; + case ValidationMode.Callback: + return onClientValidateServerPubKey(pubKeyInfo); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override bool Available() => inner.Available(); + + public override bool ClientConnected() => _client != null && _client.IsReady; + + public override void ClientConnect(string address) + { + switch (clientValidateServerPubKey) + { + case ValidationMode.Off: + break; + case ValidationMode.List: + if (clientTrustedPubKeySignatures == null || clientTrustedPubKeySignatures.Length == 0) + { + OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty."); + return; + } + break; + case ValidationMode.Callback: + if (onClientValidateServerPubKey == null) + { + OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set"); + return; + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + _credentials = EncryptionCredentials.Generate(); + inner.OnClientConnected = HandleInnerClientConnected; + inner.OnClientDataReceived = HandleInnerClientDataReceived; + inner.OnClientDataSent = (bytes, channel) => OnClientDataSent?.Invoke(bytes, channel); + inner.OnClientError = HandleInnerClientError; + inner.OnClientDisconnected = HandleInnerClientDisconnected; + inner.ClientConnect(address); + } + + public override void ClientSend(ArraySegment segment, int channelId = Channels.Reliable) => + _client?.Send(segment, channelId); + + public override void ClientDisconnect() => inner.ClientDisconnect(); + + public override Uri ServerUri() => inner.ServerUri(); + + public override bool ServerActive() => inner.ServerActive(); + + public override void ServerStart() + { + if (serverLoadKeyPairFromFile) + { + _credentials = EncryptionCredentials.LoadFromFile(serverKeypairPath); + } + else + { + _credentials = EncryptionCredentials.Generate(); + } + inner.OnServerConnected = HandleInnerServerConnected; + inner.OnServerDataReceived = HandleInnerServerDataReceived; + inner.OnServerDataSent = (connId, bytes, channel) => OnServerDataSent?.Invoke(connId, bytes, channel); + inner.OnServerError = HandleInnerServerError; + inner.OnServerDisconnected = HandleInnerServerDisconnected; + inner.ServerStart(); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId = Channels.Reliable) + { + if (_serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady) + { + connection.Send(segment, channelId); + } + } + + public override void ServerDisconnect(int connectionId) + { + // cleanup is done via inners disconnect event + inner.ServerDisconnect(connectionId); + } + + public override string ServerGetClientAddress(int connectionId) => inner.ServerGetClientAddress(connectionId); + + public override void ServerStop() => inner.ServerStop(); + + public override int GetMaxPacketSize(int channelId = Channels.Reliable) => + inner.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead; + + public override void Shutdown() => inner.Shutdown(); + + public override void ClientEarlyUpdate() + { + inner.ClientEarlyUpdate(); + } + + public override void ClientLateUpdate() + { + inner.ClientLateUpdate(); + Profiler.BeginSample("EncryptionTransport.ServerLateUpdate"); + _client?.TickNonReady(NetworkTime.localTime); + Profiler.EndSample(); + } + + public override void ServerEarlyUpdate() + { + inner.ServerEarlyUpdate(); + } + + public override void ServerLateUpdate() + { + inner.ServerLateUpdate(); + Profiler.BeginSample("EncryptionTransport.ServerLateUpdate"); + // Reverse iteration as entries can be removed while updating + for (int i = _serverPendingConnections.Count - 1; i >= 0; i--) + { + _serverPendingConnections[i].TickNonReady(NetworkTime.time); + } + Profiler.EndSample(); + } + } + +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta new file mode 100644 index 000000000..98a36edd2 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0aa135acc32a4383ae9a5817f018cb06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs new file mode 100644 index 000000000..d98906131 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs @@ -0,0 +1,9 @@ +using System; +using Org.BouncyCastle.Crypto; + +public struct PubKeyInfo +{ + public string Fingerprint; + public ArraySegment Serialized; + public AsymmetricKeyParameter Key; +} diff --git a/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta new file mode 100644 index 000000000..7b824c217 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e1e3744418024c02acf39f44c1d1bd20 +timeCreated: 1708874062 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP/KcpTransport.cs b/Assets/Mirror/Transports/KCP/KcpTransport.cs index 6b9e8fee5..1073747dd 100644 --- a/Assets/Mirror/Transports/KCP/KcpTransport.cs +++ b/Assets/Mirror/Transports/KCP/KcpTransport.cs @@ -365,7 +365,7 @@ protected virtual void OnLogStatistics() } } - public override string ToString() => $"KCP {port}"; + public override string ToString() => $"KCP [{port}]"; } } //#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/Latency/LatencySimulation.cs b/Assets/Mirror/Transports/Latency/LatencySimulation.cs index 894d3fe0f..c286326a7 100644 --- a/Assets/Mirror/Transports/Latency/LatencySimulation.cs +++ b/Assets/Mirror/Transports/Latency/LatencySimulation.cs @@ -258,7 +258,7 @@ public override void ClientLateUpdate() #endif { // send and eat - wrap.ClientSend(new ArraySegment(message.bytes), Channels.Reliable); + wrap.ClientSend(new ArraySegment(message.bytes), Channels.Unreliable); unreliableClientToServer.RemoveAt(i); --i; } @@ -303,7 +303,7 @@ public override void ServerLateUpdate() #endif { // send and eat - wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), Channels.Reliable); + wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), Channels.Unreliable); unreliableServerToClient.RemoveAt(i); --i; } diff --git a/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs b/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs index 7b57441e6..3ef97d755 100644 --- a/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs +++ b/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs @@ -61,9 +61,10 @@ public ushort Port { // prevent log flood from OnGUI or similar per-frame updates alreadyWarned = true; - Debug.LogWarning($"MultiplexTransport: Server cannot set the same listen port for all transports! Set them directly instead."); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Multiplexer] Server cannot set the same listen port for all transports! Set them directly instead."); + Console.ResetColor(); } - else { // We can't set the same port for all transports because @@ -97,10 +98,11 @@ public void RemoveFromLookup(int originalConnectionId, int transportIndex) { // remove from both KeyValuePair pair = new KeyValuePair(originalConnectionId, transportIndex); - int multiplexedId = originalToMultiplexedId[pair]; - - originalToMultiplexedId.Remove(pair); - multiplexedToOriginalId.Remove(multiplexedId); + if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId)) + { + originalToMultiplexedId.Remove(pair); + multiplexedToOriginalId.Remove(multiplexedId); + } } public bool OriginalId(int multiplexId, out int originalConnectionId, out int transportIndex) @@ -121,7 +123,10 @@ public bool OriginalId(int multiplexId, out int originalConnectionId, out int tr public int MultiplexId(int originalConnectionId, int transportIndex) { KeyValuePair pair = new KeyValuePair(originalConnectionId, transportIndex); - return originalToMultiplexedId[pair]; + if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId)) + return multiplexedId; + else + return 0; } //////////////////////////////////////////////////////////////////////// @@ -265,6 +270,19 @@ void AddServerCallbacks() { // invoke Multiplex event with multiplexed connectionId int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + if (multiplexedId == 0) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + Console.ResetColor(); + } + else + Debug.LogWarning($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + + return; + } OnServerDataReceived.Invoke(multiplexedId, data, channel); }; @@ -272,6 +290,19 @@ void AddServerCallbacks() { // invoke Multiplex event with multiplexed connectionId int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + if (multiplexedId == 0) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + Console.ResetColor(); + } + else + Debug.LogError($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + + return; + } OnServerError.Invoke(multiplexedId, error, reason); }; @@ -279,6 +310,19 @@ void AddServerCallbacks() { // invoke Multiplex event with multiplexed connectionId int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + if (multiplexedId == 0) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + Console.ResetColor(); + } + else + Debug.LogWarning($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + + return; + } OnServerDisconnected.Invoke(multiplexedId); RemoveFromLookup(originalConnectionId, transportIndex); }; @@ -336,12 +380,12 @@ public override void ServerStart() if (Utils.IsHeadless()) { Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Server listening on port {portTransport.Port}"); + Console.WriteLine($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}"); Console.ResetColor(); } else { - Debug.Log($"Server listening on port {portTransport.Port}"); + Debug.Log($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}"); } } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs index 579c41ed2..7821dede2 100644 --- a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs @@ -72,7 +72,10 @@ public bool TryHandshake(Connection conn, Uri uri) if (responseKey != expectedResponse) { - Log.Error($"[SWT-ClientHandshake]: Response key incorrect\nResponse:{responseKey}\nExpected:{expectedResponse}"); + Log.Error($"[SWT-ClientHandshake]: Response key incorrect\n" + + $"Expected:{expectedResponse}\n" + + $"Response:{responseKey}\n" + + $"This can happen if Websocket Protocol is not installed in Windows Server Roles."); return false; } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib index 02e6b936a..2fdefc382 100644 --- a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib @@ -1,61 +1,70 @@ // this will create a global object -const SimpleWeb = { +const SimpleWeb = +{ webSockets: [], next: 1, - GetWebSocket: function (index) { + GetWebSocket: function (index) + { return SimpleWeb.webSockets[index] }, - AddNextSocket: function (webSocket) { + AddNextSocket: function (webSocket) + { var index = SimpleWeb.next; SimpleWeb.next++; SimpleWeb.webSockets[index] = webSocket; return index; }, - RemoveSocket: function (index) { + RemoveSocket: function (index) + { SimpleWeb.webSockets[index] = undefined; }, }; -function IsConnected(index) { +function IsConnected(index) +{ var webSocket = SimpleWeb.GetWebSocket(index); - if (webSocket) { + if (webSocket) return webSocket.readyState === webSocket.OPEN; - } - else { + else return false; - } } -function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) { +function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) +{ // fix for unity 2021 because unity bug in .jslib - if (typeof Runtime === "undefined") { + if (typeof Runtime === "undefined") + { // if unity doesn't create Runtime, then make it here // dont ask why this works, just be happy that it does - Runtime = { - dynCall: dynCall - } + var Runtime = { dynCall: dynCall } } const address = UTF8ToString(addressPtr); console.log("Connecting to " + address); + // Create webSocket connection. - webSocket = new WebSocket(address); + var webSocket = new WebSocket(address); webSocket.binaryType = 'arraybuffer'; + const index = SimpleWeb.AddNextSocket(webSocket); // Connection opened - webSocket.addEventListener('open', function (event) { + webSocket.addEventListener('open', function (event) + { console.log("Connected to " + address); Runtime.dynCall('vi', openCallbackPtr, [index]); }); - webSocket.addEventListener('close', function (event) { + webSocket.addEventListener('close', function (event) + { console.log("Disconnected from " + address); Runtime.dynCall('vi', closeCallBackPtr, [index]); }); // Listen for messages - webSocket.addEventListener('message', function (event) { - if (event.data instanceof ArrayBuffer) { + webSocket.addEventListener('message', function (event) + { + if (event.data instanceof ArrayBuffer) + { // TODO dont alloc each time var array = new Uint8Array(event.data); var arrayLength = array.length; @@ -67,14 +76,15 @@ function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackP Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]); _free(bufferPtr); } - else { + else + { console.error("message type not supported") } }); - webSocket.addEventListener('error', function (event) { + webSocket.addEventListener('error', function (event) + { console.error('Socket Error', event); - Runtime.dynCall('vi', errorCallbackPtr, [index]); }); @@ -83,16 +93,16 @@ function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackP function Disconnect(index) { var webSocket = SimpleWeb.GetWebSocket(index); - if (webSocket) { + if (webSocket) webSocket.close(1000, "Disconnect Called by Mirror"); - } SimpleWeb.RemoveSocket(index); } function Send(index, arrayPtr, offset, length) { var webSocket = SimpleWeb.GetWebSocket(index); - if (webSocket) { + if (webSocket) + { const start = arrayPtr + offset; const end = start + length; const data = HEAPU8.buffer.slice(start, end); @@ -102,13 +112,14 @@ function Send(index, arrayPtr, offset, length) { return false; } - -const SimpleWebLib = { +const SimpleWebLib = +{ $SimpleWeb: SimpleWeb, IsConnected, Connect, Disconnect, Send }; + autoAddDeps(SimpleWebLib, '$SimpleWeb'); mergeInto(LibraryManager.library, SimpleWebLib); diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs index 5990bc3e8..093e0f53c 100644 --- a/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs @@ -144,6 +144,12 @@ public override void Shutdown() string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme; + public override bool IsEncrypted => ClientConnected() && (clientUseWss || sslEnabled) || ServerActive() && sslEnabled; + + // Not technically correct, but there's no good way to get the actual cipher, especially in browser + // When using reverse proxy, connection between proxy and server is not encrypted. + public override string EncryptionCipher => "TLS"; + public override bool ClientConnected() { // not null and not NotConnected (we want to return true if connecting or disconnecting) diff --git a/FUNDING.yml b/FUNDING.yml index 9f35c823c..f3b04ea76 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1 +1 @@ -github: vis2k +github: miwarnec diff --git a/Packages/manifest.json b/Packages/manifest.json index 04e067792..aa9ec5501 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,11 +2,12 @@ "dependencies": { "com.unity.2d.sprite": "1.0.0", "com.unity.2d.tilemap": "1.0.0", - "com.unity.ide.rider": "3.0.24", - "com.unity.ide.visualstudio": "2.0.18", + "com.unity.ide.rider": "3.0.27", + "com.unity.ide.visualstudio": "2.0.22", "com.unity.ide.vscode": "1.2.5", + "com.unity.nuget.newtonsoft-json": "3.2.1", "com.unity.test-framework": "1.1.33", - "com.unity.testtools.codecoverage": "1.2.4", + "com.unity.testtools.codecoverage": "1.2.5", "com.unity.toolchain.macos-arm64-linux-x86_64": "1.0.1", "com.unity.ugui": "1.0.0", "com.unity.xr.legacyinputhelpers": "2.1.10", @@ -40,8 +41,7 @@ "com.unity.modules.video": "1.0.0", "com.unity.modules.vr": "1.0.0", "com.unity.modules.wind": "1.0.0", - "com.unity.modules.xr": "1.0.0", - "com.unity.nuget.newtonsoft-json": "3.2.1" + "com.unity.modules.xr": "1.0.0" }, "testables": [ "com.unity.test-framework.performance" diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index d28d9a02e..6ada93dd6 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -20,7 +20,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "3.0.24", + "version": "3.0.27", "depth": 0, "source": "registry", "dependencies": { @@ -29,7 +29,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.18", + "version": "2.0.22", "depth": 0, "source": "registry", "dependencies": { @@ -86,7 +86,7 @@ "url": "https://packages.unity.com" }, "com.unity.testtools.codecoverage": { - "version": "1.2.4", + "version": "1.2.5", "depth": 0, "source": "registry", "dependencies": { diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 0a2d4cfea..7dbeb5b6c 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -48,6 +48,7 @@ PlayerSettings: defaultScreenHeightWeb: 600 m_StereoRenderingPath: 0 m_ActiveColorSpace: 0 + unsupportedMSAAFallback: 0 m_MTRendering: 1 mipStripping: 0 numberOfMipsStripped: 0 @@ -74,6 +75,7 @@ PlayerSettings: androidMinimumWindowWidth: 400 androidMinimumWindowHeight: 300 androidFullscreenMode: 1 + androidAutoRotationBehavior: 1 defaultIsNativeResolution: 1 macRetinaSupport: 1 runInBackground: 1 @@ -121,6 +123,7 @@ PlayerSettings: switchNVNOtherPoolsGranularity: 16777216 switchNVNMaxPublicTextureIDCount: 0 switchNVNMaxPublicSamplerIDCount: 0 + switchMaxWorkerMultiple: 8 stadiaPresentMode: 0 stadiaTargetFramerate: 0 vulkanNumSwapchainBuffers: 3 @@ -346,7 +349,7 @@ PlayerSettings: switchSocketConcurrencyLimit: 14 switchScreenResolutionBehavior: 2 switchUseCPUProfiler: 0 - switchUseGOLDLinker: 0 + switchEnableFileSystemTrace: 0 switchLTOSetting: 0 switchApplicationID: 0x01004b9000490000 switchNSODependencies: @@ -475,7 +478,6 @@ PlayerSettings: switchSocketBufferEfficiency: 4 switchSocketInitializeEnabled: 1 switchNetworkInterfaceManagerInitializeEnabled: 1 - switchPlayerConnectionEnabled: 1 switchUseNewStyleFilepaths: 0 switchUseLegacyFmodPriorities: 1 switchUseMicroSleepForYield: 1 @@ -580,7 +582,7 @@ PlayerSettings: webGLPowerPreference: 2 scriptingDefineSymbols: Server: MIRROR;MIRROR_70_OR_NEWER;MIRROR_71_OR_NEWER;MIRROR_73_OR_NEWER;MIRROR_78_OR_NEWER;MIRROR_79_OR_NEWER;MIRROR_81_OR_NEWER;MIRROR_82_OR_NEWER;MIRROR_83_OR_NEWER;MIRROR_84_OR_NEWER;MIRROR_85_OR_NEWER - Standalone: MIRROR;MIRROR_70_OR_NEWER;MIRROR_71_OR_NEWER;MIRROR_73_OR_NEWER;MIRROR_78_OR_NEWER;MIRROR_79_OR_NEWER;MIRROR_81_OR_NEWER;MIRROR_82_OR_NEWER;MIRROR_83_OR_NEWER;MIRROR_84_OR_NEWER;MIRROR_85_OR_NEWER;MIRROR_86_OR_NEWER + Standalone: MIRROR;MIRROR_70_OR_NEWER;MIRROR_71_OR_NEWER;MIRROR_73_OR_NEWER;MIRROR_78_OR_NEWER;MIRROR_79_OR_NEWER;MIRROR_81_OR_NEWER;MIRROR_82_OR_NEWER;MIRROR_83_OR_NEWER;MIRROR_84_OR_NEWER;MIRROR_85_OR_NEWER;MIRROR_86_OR_NEWER;MIRROR_89_OR_NEWER WebGL: MIRROR;MIRROR_70_OR_NEWER;MIRROR_71_OR_NEWER;MIRROR_73_OR_NEWER;MIRROR_78_OR_NEWER;MIRROR_79_OR_NEWER;MIRROR_81_OR_NEWER;MIRROR_82_OR_NEWER;MIRROR_83_OR_NEWER;MIRROR_84_OR_NEWER;MIRROR_85_OR_NEWER additionalCompilerArguments: {} platformArchitecture: {} diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index db3863a92..39a629e9d 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2021.3.29f1 -m_EditorVersionWithRevision: 2021.3.29f1 (204d6dc9ae1c) +m_EditorVersion: 2021.3.35f1 +m_EditorVersionWithRevision: 2021.3.35f1 (157b46ce122a) diff --git a/README.md b/README.md index 95b9a48e4..d8cba5fa0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Many of our features quickly became the norm across all Unity netcodes!
| | | | | 📏 **Snapshot Interp.** | Perfectly smooth movement for all platforms and all games. | **Stable** | | 🏎 **Fast Prediction** | Simulate Physics locally & apply server corrections **[VR ready]** | **Beta** | -| 🔫 **Lag Compensation** | Roll back state to see what the player saw during input. | **Preview** | +| 🔫 **Lag Compensation** | Roll back state to see what the player saw during input. | **Beta** | | | | | | 🧙‍♂️ **General Purpose** | Mirror supports all genres for all your games! | | | 🧘‍♀️ **Stable API** | Long term (10 years) stability instead of new versions! | @@ -58,9 +58,8 @@ Many of our features quickly became the norm across all Unity netcodes!
| ❤️ **Community** | Join our Discord with nearly 15.000 developers world wide! | | | 🧜🏻‍♀️ **Long Term Support** | Maintained since 2014 with optional LTS version! | | | | | | -| 🦖 **Deterministic Physics** | Open source deterministic physics for C# & Unity! | **Researching** | +| 🔒 **Encryption** | Secure communication with end-to-end encryption. | **Preview** | | 📐 **Bitpacking** | Optimized compression (bools as 1 bit etc.) | **Researching** | -| 🔒 **Encryption** | Secure communication with end-to-end encryption. | **Researching** | --- ## Architecture @@ -144,7 +143,7 @@ Without any breaking changes, ever! [![Population: ONE](https://github.com/MirrorNetworking/Mirror/assets/16416509/dddc778b-a97f-452d-b5f8-6ec42c6da4f1)](https://www.populationonevr.com/) The [BigBoxVR](https://www.bigboxvr.com/) team started using Mirror in February 2019 for what eventually became one of the most popular Oculus Rift games. -In addition to [24/7 support](https://github.com/sponsors/vis2k) from the Mirror team, BigBoxVR also hired one of our engineers. +In addition to [24/7 support](https://discordapp.com/invite/xVW4nU4C34) from the Mirror team, BigBoxVR also hired one of our engineers. **Population: ONE** was [acquired by Meta](https://uploadvr.com/population-one-facebook-bigbox-acquire/) in June 2021, and they've just released a new [Sandbox](https://www.youtube.com/watch?v=jcI0h8dn9tA) addon in 2022! @@ -162,6 +161,12 @@ SWARM is a fast-paced, arcade-style grapple shooter, with quick sessions, bright Available for the [Meta Quest](https://www.oculus.com/experiences/quest/2236053486488156/), made with Mirror. +### [Castaways](https://www.castaways.com/) +[![Castaways](https://user-images.githubusercontent.com/16416509/207313082-e6b95590-80c6-4685-b0d1-f1c39c236316.png)](https://www.castaways.com/) +[Castaways](https://www.castaways.com/) is a sandbox game where you are castaway to a small remote island where you must work with others to survive and build a thriving new civilization. + +Castaway runs in the Browser, thanks to Mirror's WebGL support. + ### [Nimoyd](https://www.nimoyd.com/) [![nimoyd_smaller](https://user-images.githubusercontent.com/16416509/178142672-340bac2c-628a-4610-bbf1-8f718cb5b033.jpg)](https://www.nimoyd.com/) Nudge Nudge Games' first title: the colorful, post-apocalyptic open world sandbox game [Nimoyd](https://store.steampowered.com/app/1313210/Nimoyd__Survival_Sandbox/) is being developed with Mirror. @@ -188,6 +193,10 @@ James Bendon initially made the game with UNET, and then [switched to Mirror](ht Made with Mirror by two brothers with [no prior game development](https://www.youtube.com/watch?v=5J2wj8l4pFA&start=12) experience. +### [Havoc](https://store.steampowered.com/app/2149290/Havoc/) +![havoc fps game](https://github.com/MirrorNetworking/Mirror/assets/16416509/f3549a95-5663-41f8-9868-283b3a0fcf63) +Havoc is a tactical team-based first-person shooter with a fully destructible environment and a unique art style. Havoc has been one of our favorite made-with-Mirror games for a few years now, and we are excited to finally see it up there on Steam. + ### [Sun Haven](https://store.steampowered.com/app/1432860/Sun_Haven/) [![sun haven](https://user-images.githubusercontent.com/16416509/185836661-2bfd6cd0-523a-4af4-bac7-c202ed01de7d.jpg)](https://store.steampowered.com/app/1432860/Sun_Haven/) [Sun Haven](https://store.steampowered.com/app/1432860/Sun_Haven/) - A beautiful human town, a hidden elven village, and a monster city filled with farming, magic, dragons, and adventure. @@ -248,12 +257,6 @@ The [France based team](https://naicaonline.com/en/news/view/1) was one of Mirro [![Empires Mobile](https://user-images.githubusercontent.com/16416509/207028553-c646f12c-c164-47d3-a1fc-ff79409c04fa.jpg)](https://knightempire.online/) [Empires Mobile](https://knightempire.online/) - Retro mobile MMORPG for Android and iOS, reaching 5000 CCU at times. Check out their [video](https://www.youtube.com/watch?v=v69lW9aWb-w) for some _early MMORPG_ nostalgia. -### [Castaways](https://www.castaways.com/) -[![Castaways](https://user-images.githubusercontent.com/16416509/207313082-e6b95590-80c6-4685-b0d1-f1c39c236316.png)](https://www.castaways.com/) -[Castaways](https://www.castaways.com/) is a sandbox game where you are castaway to a small remote island where you must work with others to survive and build a thriving new civilization. - -Castaway runs in the Browser, thanks to Mirror's WebGL support. - ### [Overpowered](https://overpoweredcardgame.com/) [![Overpowered](https://github.com/MirrorNetworking/Mirror/assets/16416509/5bdbb227-970d-434e-b062-94fde1297f7c)](https://overpoweredcardgame.com/) [Overwpowered](https://overpoweredcardgame.com/), the exciting new card game that combines strategy, myth, and fun into one riveting web-based experience. Launched in 2023, made with Mirror! @@ -273,6 +276,7 @@ Castaway runs in the Browser, thanks to Mirror's WebGL support. + ## Modular Transports Mirror uses **KCP** (reliable UDP) by default, but you may use any of our community transports for low level packet sending: