diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs index b81433773..78e63a8f5 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -42,6 +42,16 @@ public class NetworkTransformReliable : NetworkTransformBase // Used to store last sent snapshots protected TransformSnapshot last; + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to reliable + syncMethod = SyncMethod.Reliable; + } + // update ////////////////////////////////////////////////////////////// void Update() { diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs index 235a65a1d..3d16bb72f 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -30,6 +30,16 @@ public class NetworkTransformUnreliable : NetworkTransformBase protected Changed cachedChangedComparison; protected bool hasSentUnchangedPosition; + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to unreliable + syncMethod = SyncMethod.Unreliable; + } + // update ////////////////////////////////////////////////////////////// // Update applies interpolation void Update() @@ -316,7 +326,7 @@ protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnaps } if (syncRotation) - { + { if (compressRotation) { bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; @@ -459,7 +469,7 @@ protected virtual void UpdateSyncData(ref SyncData syncData, SortedList receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale) { using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload)) diff --git a/Assets/Mirror/Core/Messages.cs b/Assets/Mirror/Core/Messages.cs index 62888c07e..569234073 100644 --- a/Assets/Mirror/Core/Messages.cs +++ b/Assets/Mirror/Core/Messages.cs @@ -94,6 +94,7 @@ public struct ObjectHideMessage : NetworkMessage public uint netId; } + // state update for reliable sync public struct EntityStateMessage : NetworkMessage { public uint netId; @@ -102,6 +103,15 @@ public struct EntityStateMessage : NetworkMessage public ArraySegment payload; } + // state update for unreliable sync + public struct EntityStateMessageUnreliable : NetworkMessage + { + public uint netId; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + // whoever wants to measure rtt, sends this to the other end. public struct NetworkPingMessage : NetworkMessage { diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index f76a4a389..841cf4aa4 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -6,6 +6,11 @@ namespace Mirror { + // SyncMethod to choose between: + // * Reliable: oldschool reliable sync every syncInterval. If nothing changes, nothing is sent. + // * Unreliable: quake style unreliable state sync & delta compression, for fast paced games. + public enum SyncMethod { Reliable, Unreliable } + // SyncMode decides if a component is synced to all observers, or only owner public enum SyncMode { Observers, Owner } @@ -24,6 +29,9 @@ public enum SyncDirection { ServerToClient, ClientToServer } [HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")] public abstract class NetworkBehaviour : MonoBehaviour { + [Tooltip("Choose between:\n- Reliable: only sends when changed. Recommended for most games!\n- Unreliable: immediately sends at the expense of bandwidth. Only for hardcore competitive games.\nClick the Help icon for full details.")] + [HideInInspector] public SyncMethod syncMethod = SyncMethod.Reliable; + /// Sync direction for OnSerialize. ServerToClient by default. ClientToServer for client authority. [Tooltip("Server Authority calls OnSerialize on the server and syncs it to clients.\n\nClient Authority calls OnSerialize on the owning client, syncs it to server, which then broadcasts it to all other clients.\n\nUse server authority for cheat safety.")] [HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient; @@ -1077,7 +1085,7 @@ protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehavio { return null; } - + // ensure componentIndex is in range. // show explicit errors if something went wrong, instead of IndexOutOfRangeException. // removing components at runtime isn't allowed, yet this happened in a project so we need to check for it. diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index 2aff2d47b..d21d0bb9b 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -33,6 +33,12 @@ public static partial class NetworkClient public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms static double lastSendTime; + // ocassionally send a full reliable state for unreliable components to delta compress against. + // this only applies to Components with SyncMethod=Unreliable. + public static int unreliableBaselineRate => NetworkServer.unreliableBaselineRate; + public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms + static double lastUnreliableBaselineTime; + // For security, it is recommended to disconnect a player if a networked // action triggers an exception\nThis could prevent components being // accessed in an undefined state, which may be an attack vector for @@ -1547,6 +1553,7 @@ internal static void NetworkLateUpdate() // // Unity 2019 doesn't have Time.timeAsDouble yet bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime); if (!Application.isPlaying || sendIntervalElapsed) { Broadcast(); diff --git a/Assets/Mirror/Core/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs index 301587904..5c9a33c19 100644 --- a/Assets/Mirror/Core/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -42,6 +42,10 @@ public class NetworkManager : MonoBehaviour [FormerlySerializedAs("serverTickRate")] public int sendRate = 60; + /// + [Tooltip("Ocassionally send a full reliable state for unreliable components to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] + public int unreliableBaselineRate = 1; + // Deprecated 2023-11-25 // Using SerializeField and HideInInspector to self-correct for being // replaced by headlessStartMode. This can be removed in the future. @@ -199,6 +203,11 @@ public virtual void OnValidate() autoConnectClientBuild = false; #pragma warning restore 618 + // unreliable full send rate needs to be >= 0. + // we need to have something to delta compress against. + // it should also be <= sendRate otherwise there's no point. + unreliableBaselineRate = Mathf.Clamp(unreliableBaselineRate, 1, sendRate); + // always >= 0 maxConnections = Mathf.Max(maxConnections, 0); @@ -308,6 +317,7 @@ bool IsServerOnlineSceneChangeNeeded() => void ApplyConfiguration() { NetworkServer.tickRate = sendRate; + NetworkServer.unreliableBaselineRate = unreliableBaselineRate; NetworkClient.snapshotSettings = snapshotSettings; NetworkClient.connectionQualityInterval = evaluationInterval; NetworkClient.connectionQualityMethod = evaluationMethod; diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 25d59548e..937bb6d83 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -54,6 +54,12 @@ public static partial class NetworkServer public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms static double lastSendTime; + // ocassionally send a full reliable state for unreliable components to delta compress against. + // this only applies to Components with SyncMethod=Unreliable. + public static int unreliableBaselineRate = 1; + public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms + static double lastUnreliableBaselineTime; + /// Connection to host mode client (if any) public static LocalConnectionToClient localConnection { get; private set; } @@ -2053,6 +2059,7 @@ internal static void NetworkLateUpdate() // snapshots _but_ not every single tick. // Unity 2019 doesn't have Time.timeAsDouble yet bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime); if (!Application.isPlaying || sendIntervalElapsed) Broadcast(); } diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index 52c56d6b6..f45742029 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -94,6 +94,16 @@ protected void DrawDefaultSyncSettings() if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient) EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + // sync method + SerializedProperty syncMethod = serializedObject.FindProperty("syncMethod"); + EditorGUILayout.PropertyField(syncMethod); + + // Unreliable sync method: show a warning! + if (syncMethod.enumValueIndex == (int)SyncMethod.Unreliable) + { + EditorGUILayout.HelpBox("Beware!\nUnreliable is experimental and only meant for hardcore competitive games!", MessageType.Warning); + } + // sync interval EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval"));