diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs new file mode 100644 index 000000000..d003d19bd --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs @@ -0,0 +1,303 @@ +// NetworkTransform V3 based on NetworkTransformUnreliable, using Mirror's new +// Unreliable quake style networking model with delta compression. +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Unreliable Compressed)")] + public class NetworkTransformUnreliableCompressed : NetworkTransformBase + { + [Header("Debug")] + public bool debugDraw = false; + + // 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 unreliable + syncMethod = SyncMethod.Unreliable; + + // Unreliable ignores syncInterval. don't need to force anymore: + // sendIntervalMultiplier = 1; + } + + // update ////////////////////////////////////////////////////////////// + void Update() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + + void LateUpdate() + { + // set dirty to trigger OnSerialize. either always, or only if changed. + // It has to be checked in LateUpdate() for onlySyncOnChange to avoid + // the possibility of Update() running first before the object's movement + // script's Update(), which then causes NT to send every alternate frame + // instead. + if (isServer || (IsClientWithAuthority && NetworkClient.ready)) + { + if (!onlySyncOnChange) + SetDirty(); + } + } + + protected virtual void UpdateServer() + { + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> don't apply for host mode player objects either, even if in + // client authority mode. if it doesn't go over the network, + // then we don't need to do anything. + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + } + } + } + + protected virtual void UpdateClient() + { + // client authority, and local player (= allowed to move myself)? + if (!IsClientWithAuthority) + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + + if (debugDraw) + { + Debug.DrawLine(from.position, to.position, Color.white, 10f); + Debug.DrawLine(computed.position, computed.position + Vector3.up, Color.white, 10f); + } + } + } + } + + // Unreliable OnSerialize: + // - initial=true sends reliable full state + // - initial=false sends unreliable delta states + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // get current snapshot for broadcasting. + TransformSnapshot snapshot = Construct(); + + // ClientToServer optimization: + // for interpolated client owned identities, + // always broadcast the latest known snapshot so other clients can + // interpolate immediately instead of catching up too + + // TODO dirty mask? [compression is very good w/o it already] + // each vector's component is delta compressed. + // an unchanged component would still require 1 byte. + // let's use a dirty bit mask to filter those out as well. + + // Debug.Log($"NT OnSerialize: initial={initialState} method={syncMethod}"); + + // reliable full state + if (initialState) + { + // TODO initialState is now sent multiple times. find a new fix for this: + // If there is a last serialized snapshot, we use it. + // This prevents the new client getting a snapshot that is different + // from what the older clients last got. If this happens, and on the next + // regular serialisation the delta compression will get wrong values. + // Notes: + // 1. Interestingly only the older clients have it wrong, because at the end + // of this function, last = snapshot which is the initial state's snapshot + // 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate + // snapshot constructed would have been the same as the last anyway. + // if (last.remoteTime > 0) snapshot = last; + + int startPosition = writer.Position; + + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // if smallest-three quaternion compression is enabled, + // then we don't need baseline rotation since delta always + // sends an absolute value. + if (!compressRotation) + { + writer.WriteQuaternion(snapshot.rotation); + } + } + if (syncScale) writer.WriteVector3(snapshot.scale); + + // set 'last' + last = snapshot; + } + // unreliable delta: compress against last full reliable state + else + { + int startPosition = writer.Position; + + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + { + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + } + else writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) writer.WriteVector3(snapshot.scale); + } + } + + // Unreliable OnDeserialize: + // - initial=true sends reliable full state + // - initial=false sends unreliable delta states + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + // reliable full state + if (initialState) + { + if (syncPosition) + { + position = reader.ReadVector3(); + + if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.green, 10.0f); + } + if (syncRotation) + { + // if smallest-three quaternion compression is enabled, + // then we don't need baseline rotation since delta always + // sends an absolute value. + if (!compressRotation) + { + rotation = reader.ReadQuaternion(); + } + } + if (syncScale) scale = reader.ReadVector3(); + } + // unreliable delta: decompress against last full reliable state + else + { + // varint -> delta -> quantize + if (syncPosition) + { + position = reader.ReadVector3(); + + if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.yellow, 10.0f); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + { + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + } + else + { + rotation = reader.ReadQuaternion(); + } + } + if (syncScale) + { + scale = reader.ReadVector3(); + } + + // handle depending on server / client / host. + // server has priority for host mode. + // + // only do this for the unreliable delta states! + // processing the reliable baselines shows noticeable jitter + // around baseline syncs (e.g. tanks demo @ 4 Hz sendRate). + // unreliable deltas are always within the same time delta, + // so this gives perfectly smooth results. + if (isServer) OnClientToServerSync(position, rotation, scale); + else if (isClient) OnServerToClientSync(position, rotation, scale); + } + } + + // sync //////////////////////////////////////////////////////////////// + + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); + } + + // reset state for next session. + // do not ever call this during a session (i.e. after teleport). + // calling this will break delta compression. + public override void ResetState() + { + base.ResetState(); + + // reset 'last' for delta too + last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero); + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta new file mode 100644 index 000000000..5eef915ca --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliableCompressed.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d993bac37a92145448c1ea027b3e9ddc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Core/Messages.cs b/Assets/Mirror/Core/Messages.cs index 569234073..88389417a 100644 --- a/Assets/Mirror/Core/Messages.cs +++ b/Assets/Mirror/Core/Messages.cs @@ -103,9 +103,32 @@ public struct EntityStateMessage : NetworkMessage public ArraySegment payload; } - // state update for unreliable sync - public struct EntityStateMessageUnreliable : NetworkMessage + // state update for unreliable sync. + // baseline is always sent over Reliable channel. + public struct EntityStateMessageUnreliableBaseline : NetworkMessage { + // baseline messages send their tick number as byte. + // delta messages are checked against that tick to avoid applying a + // delta on top of the wrong baseline. + // (byte is enough, we just need something small to compare against) + public byte baselineTick; + + public uint netId; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + // state update for unreliable sync + // delta is always sent over Unreliable channel. + public struct EntityStateMessageUnreliableDelta : NetworkMessage + { + // baseline messages send their tick number as byte. + // delta messages are checked against that tick to avoid applying a + // delta on top of the wrong baseline. + // (byte is enough, we just need something small to compare against) + public byte baselineTick; + public uint netId; // the serialized component data // -> ArraySegment to avoid unnecessary allocations diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index 841cf4aa4..8375019de 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -236,12 +236,17 @@ public bool IsDirty() => // only check time if bits were dirty. this is more expensive. NetworkTime.localTime - lastSyncTime >= syncInterval; + // true if any SyncVar or SyncObject is dirty + // OR both bitmasks. != 0 if either was dirty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty_BitsOnly() => (syncVarDirtyBits | syncObjectDirtyBits) != 0UL; + /// Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits) // automatically invoked when an update is sent for this object, but can // be called manually as well. - public void ClearAllDirtyBits() + public void ClearAllDirtyBits(bool clearSyncTime = true) { - lastSyncTime = NetworkTime.localTime; + if (clearSyncTime) lastSyncTime = NetworkTime.localTime; syncVarDirtyBits = 0L; syncObjectDirtyBits = 0L; diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index d21d0bb9b..a59dbb510 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -36,9 +36,14 @@ public static partial class NetworkClient // 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 + public static float unreliableBaselineInterval => NetworkServer.unreliableBaselineInterval; static double lastUnreliableBaselineTime; + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + public static bool unreliableRedundancy => NetworkServer.unreliableRedundancy; + // 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 @@ -511,6 +516,8 @@ internal static void RegisterMessageHandlers(bool hostMode) RegisterHandler(_ => { }); // host mode doesn't need state updates RegisterHandler(_ => { }); + RegisterHandler(_ => { }); + RegisterHandler(_ => { }); } else { @@ -522,6 +529,8 @@ internal static void RegisterMessageHandlers(bool hostMode) RegisterHandler(OnObjectSpawnStarted); RegisterHandler(OnObjectSpawnFinished); RegisterHandler(OnEntityStateMessage); + RegisterHandler(OnEntityStateMessageUnreliableBaseline); + RegisterHandler(OnEntityStateMessageUnreliableDelta); } // These handlers are the same for host and remote clients @@ -1440,6 +1449,90 @@ static void OnEntityStateMessage(EntityStateMessage message) else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); } + static void OnEntityStateMessageUnreliableBaseline(EntityStateMessageUnreliableBaseline message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Reliable) + { + Debug.LogError($"Client OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!"); + return; + } + + // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // set the last received reliable baseline tick number. + identity.lastUnreliableBaselineReceived = message.baselineTick; + + // iniital is always 'true' because unreliable state sync alwasy serializes full + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // full state updates (initial=true) arrive over reliable. + identity.DeserializeClient(reader, true); + } + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + + static void OnEntityStateMessageUnreliableDelta(EntityStateMessageUnreliableDelta message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Unreliable) + { + Debug.LogError($"Client OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!"); + return; + } + + // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // unreliable state sync messages may arrive out of order. + // only ever apply state that's newer than the last received state. + // note that we send one EntityStateMessage per Entity, + // so there will be multiple with the same == timestamp. + // + // note that a reliable baseline may arrive before/after a delta. + // that is fine. + if (connection.remoteTimeStamp < identity.lastUnreliableStateTime) + { + // debug log to show that it's working. + // can be tested via LatencySimulation scramble easily. + Debug.Log($"Client caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + // UDP messages may accidentally arrive twice. + // or even intentionally, if unreliableRedundancy is turned on. + else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime) + { + // only log this if unreliableRedundancy is disabled. + // otherwise it's expected and will happen a lot. + if (!unreliableRedundancy) Debug.Log($"Client caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + + // make sure this delta is for the correct baseline. + // we don't want to apply an old delta on top of a new baseline. + if (message.baselineTick != identity.lastUnreliableBaselineReceived) + { + Debug.Log($"Client caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine."); + return; + } + + // set the new last received time for unreliable + identity.lastUnreliableStateTime = connection.remoteTimeStamp; + + // iniital is always 'true' because unreliable state sync alwasy serializes full + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // delta state updates (initial=false) arrive over unreliable. + identity.DeserializeClient(reader, false); + } + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + static void OnRPCMessage(RpcMessage message) { // Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}"); @@ -1556,7 +1649,7 @@ internal static void NetworkLateUpdate() bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime); if (!Application.isPlaying || sendIntervalElapsed) { - Broadcast(); + Broadcast(unreliableBaselineElapsed); } UpdateConnectionQuality(); @@ -1620,7 +1713,9 @@ void UpdateConnectionQuality() // broadcast /////////////////////////////////////////////////////////// // make sure Broadcast() is only called every sendInterval. // calling it every update() would require too much bandwidth. - static void Broadcast() + // + // unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + static void Broadcast(bool unreliableBaselineElapsed) { // joined the world yet? if (!connection.isReady) return; @@ -1632,12 +1727,14 @@ static void Broadcast() Send(new TimeSnapshotMessage(), Channels.Unreliable); // broadcast client state to server - BroadcastToServer(); + BroadcastToServer(unreliableBaselineElapsed); } // NetworkServer has BroadcastToConnection. // NetworkClient has BroadcastToServer. - static void BroadcastToServer() + // + // unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + static void BroadcastToServer(bool unreliableBaselineElapsed) { // for each entity that the client owns foreach (NetworkIdentity identity in connection.owned) @@ -1648,21 +1745,64 @@ static void BroadcastToServer() // NetworkServer.Destroy) if (identity != null) { - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + // 'Reliable' sync: send Reliable components over reliable. + using (NetworkWriterPooled writerReliable = NetworkWriterPool.Get(), + writerUnreliableDelta = NetworkWriterPool.Get(), + writerUnreliableBaseline = NetworkWriterPool.Get()) { - // get serialization for this entity viewed by this connection - // (if anything was serialized this time) - identity.SerializeClient(writer); - if (writer.Position > 0) + // serialize reliable and unreliable components in only one iteration. + // serializing reliable and unreliable separately in two iterations would be too costly. + identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed); + + // any reliable components serialization? + if (writerReliable.Position > 0) { // send state update message EntityStateMessage message = new EntityStateMessage { netId = identity.netId, - payload = writer.ToArraySegment() + payload = writerReliable.ToArraySegment() }; Send(message); } + + // any unreliable components serialization? + // we always send unreliable deltas to ensure interpolation always has a data point that arrives immediately. + if (writerUnreliableDelta.Position > 0) + { + EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta + { + // baselineTick: the last unreliable baseline to compare against + baselineTick = identity.lastUnreliableBaselineSent, + netId = identity.netId, + payload = writerUnreliableDelta.ToArraySegment() + }; + Send(message, Channels.Unreliable); + } + + // time for unreliable baseline sync? + // we always send this after the unreliable delta, + // so there's a higher chance that it arrives after the delta. + // in other words: so that the delta can still be used against previous baseline. + if (unreliableBaselineElapsed) + { + if (writerUnreliableBaseline.Position > 0) + { + // remember last sent baseline tick for this entity. + // (byte) to minimize bandwidth. we don't need the full tick, + // just something small to compare against. + identity.lastUnreliableBaselineSent = (byte)Time.frameCount; + + // send state update message + EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline + { + baselineTick = identity.lastUnreliableBaselineSent, + netId = identity.netId, + payload = writerUnreliableBaseline.ToArraySegment() + }; + Send(message, Channels.Reliable); + } + } } } // spawned list should have no null entries because we diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs index b1c12d775..4ddb89c7a 100644 --- a/Assets/Mirror/Core/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -27,13 +27,26 @@ public struct NetworkIdentitySerialization { // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks public int tick; - public NetworkWriter ownerWriter; - public NetworkWriter observersWriter; + + // reliable sync + public NetworkWriter ownerWriterReliable; + public NetworkWriter observersWriterReliable; + + // unreliable sync + public NetworkWriter ownerWriterUnreliableBaseline; + public NetworkWriter observersWriterUnreliableBaseline; + + public NetworkWriter ownerWriterUnreliableDelta; + public NetworkWriter observersWriterUnreliableDelta; public void ResetWriters() { - ownerWriter.Position = 0; - observersWriter.Position = 0; + ownerWriterReliable.Position = 0; + observersWriterReliable.Position = 0; + ownerWriterUnreliableBaseline.Position = 0; + observersWriterUnreliableBaseline.Position = 0; + ownerWriterUnreliableDelta.Position = 0; + observersWriterUnreliableDelta.Position = 0; } } @@ -225,10 +238,23 @@ public Visibility visible // => way easier to store them per object NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization { - ownerWriter = new NetworkWriter(), - observersWriter = new NetworkWriter() + ownerWriterReliable = new NetworkWriter(), + observersWriterReliable = new NetworkWriter(), + ownerWriterUnreliableBaseline = new NetworkWriter(), + observersWriterUnreliableBaseline = new NetworkWriter(), + ownerWriterUnreliableDelta = new NetworkWriter(), + observersWriterUnreliableDelta = new NetworkWriter(), }; + // unreliable state sync messages may arrive out of order, or duplicated. + // keep latest received timestamp so we don't apply older messages. + internal double lastUnreliableStateTime; + + // the last baseline we received for this object. + // deltas are based on the baseline, need to make sure we don't apply on an old one. + internal byte lastUnreliableBaselineSent; + internal byte lastUnreliableBaselineReceived; + // Keep track of all sceneIds to detect scene duplicates static readonly Dictionary sceneIds = new Dictionary(); @@ -879,10 +905,20 @@ internal void OnStopLocalPlayer() // build dirty mask for server owner & observers (= all dirty components). // faster to do it in one iteration instead of iterating separately. - (ulong, ulong) ServerDirtyMasks_Broadcast() + // -> build Reliable and Unreliable masks in one iteration. + // running two loops would be too costly. + void ServerDirtyMasks_Broadcast( + out ulong ownerMaskReliable, out ulong observerMaskReliable, + out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline, + out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta) { - ulong ownerMask = 0; - ulong observerMask = 0; + // clear + ownerMaskReliable = 0; + observerMaskReliable = 0; + ownerMaskUnreliableBaseline = 0; + observerMaskUnreliableBaseline = 0; + ownerMaskUnreliableDelta = 0; + observerMaskUnreliableDelta = 0; NetworkBehaviour[] components = NetworkBehaviours; for (int i = 0; i < components.Length; ++i) @@ -890,35 +926,97 @@ internal void OnStopLocalPlayer() NetworkBehaviour component = components[i]; ulong nthBit = (1u << i); - bool dirty = component.IsDirty(); - - // owner needs to be considered for both SyncModes, because - // Observers mode always includes the Owner. - // - // for broadcast, only for ServerToClient and only if dirty. - // ClientToServer comes from the owner client. - if (component.syncDirection == SyncDirection.ServerToClient && dirty) - ownerMask |= nthBit; - - // observers need to be considered only in Observers mode, - // otherwise they receive no sync data of this component ever. - if (component.syncMode == SyncMode.Observers) + // RELIABLE COMPONENTS ///////////////////////////////////////// + if (component.syncMethod == SyncMethod.Reliable) { - // for broadcast, only sync to observers if dirty. - // SyncDirection is irrelevant, as both are broadcast to - // observers which aren't the owner. - if (dirty) observerMask |= nthBit; + // check if this component is dirty + bool dirty = component.IsDirty(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for broadcast, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (component.syncDirection == SyncDirection.ServerToClient && dirty) + ownerMaskReliable |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for broadcast, only sync to observers if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (dirty) observerMaskReliable |= nthBit; + } + } + // UNRELIABLE COMPONENTS /////////////////////////////////////// + else if (component.syncMethod == SyncMethod.Unreliable) + { + // UNRELIABLE DELTAS /////////////////////////////////////// + { + // check if this component is dirty. + // delta sync runs @ syncInterval. + // this allows for significant bandwidth savings. + bool dirty = component.IsDirty(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for broadcast, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (component.syncDirection == SyncDirection.ServerToClient && dirty) + ownerMaskUnreliableDelta |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for broadcast, only sync to observers if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (dirty) observerMaskUnreliableDelta |= nthBit; + } + } + // UNRELIABLE BASELINE ///////////////////////////////////// + { + // check if this component is dirty. + // baseline sync runs @ 1 Hz (netmanager configurable). + // only consider dirty bits, ignore syncinterval. + bool dirty = component.IsDirty_BitsOnly(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for broadcast, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (component.syncDirection == SyncDirection.ServerToClient && dirty) + ownerMaskUnreliableBaseline |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for broadcast, only sync to observers if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (dirty) observerMaskUnreliableBaseline |= nthBit; + } + } + //////////////////////////////////////////////////////////// } } - - return (ownerMask, observerMask); } - // build dirty mask for client. + // build dirty mask for client components. // server always knows initialState, so we don't need it here. - ulong ClientDirtyMask() + // -> build Reliable and Unreliable masks in one iteration. + // running two loops would be too costly. + void ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta) { - ulong mask = 0; + dirtyMaskReliable = 0; + dirtyMaskUnreliableBaseline = 0; + dirtyMaskUnreliableDelta = 0; NetworkBehaviour[] components = NetworkBehaviours; for (int i = 0; i < components.Length; ++i) @@ -936,13 +1034,29 @@ ulong ClientDirtyMask() if (isOwned && component.syncDirection == SyncDirection.ClientToServer) { - // set the n-th bit if dirty - // shifting from small to large numbers is varint-efficient. - if (component.IsDirty()) mask |= nthBit; + // RELIABLE COMPONENTS ///////////////////////////////////////// + if (component.syncMethod == SyncMethod.Reliable) + { + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + if (component.IsDirty()) dirtyMaskReliable |= nthBit; + } + // UNRELIABLE COMPONENTS /////////////////////////////////////// + else if (component.syncMethod == SyncMethod.Unreliable) + { + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + + // baseline sync runs @ 1 Hz (netmanager configurable). + // only consider dirty bits, ignore syncinterval. + if (component.IsDirty_BitsOnly()) dirtyMaskUnreliableBaseline |= nthBit; + + // delta sync runs @ syncInterval. + // this allows for significant bandwidth savings. + if (component.IsDirty()) dirtyMaskUnreliableDelta |= nthBit; + } } } - - return mask; } // check if n-th component is dirty. @@ -1023,7 +1137,14 @@ internal void SerializeServer_Spawn(NetworkWriter ownerWriter, NetworkWriter obs // serialize server components, with delta state for broadcast messages. // check ownerWritten/observersWritten to know if anything was written - internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter observersWriter) + // + // serialize Reliable and Unreliable components in one iteration. + // having two separate functions doing two iterations would be too costly. + internal void SerializeServer_Broadcast( + NetworkWriter ownerWriterReliable, NetworkWriter observersWriterReliable, + NetworkWriter ownerWriterUnreliableBaseline, NetworkWriter observersWriterUnreliableBaseline, + NetworkWriter ownerWriterUnreliableDelta, NetworkWriter observersWriterUnreliableDelta, + bool unreliableBaseline) { // ensure NetworkBehaviours are valid before usage ValidateComponents(); @@ -1036,16 +1157,29 @@ internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter // instead of writing a 1 byte index per component, // we limit components to 64 bits and write one ulong instead. // the ulong is also varint compressed for minimum bandwidth. - (ulong ownerMask, ulong observerMask) = ServerDirtyMasks_Broadcast(); + ServerDirtyMasks_Broadcast( + out ulong ownerMaskReliable, out ulong observerMaskReliable, + out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline, + out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta + ); // if nothing dirty, then don't even write the mask. // otherwise, every unchanged object would send a 1 byte dirty mask! - if (ownerMask != 0) Compression.CompressVarUInt(ownerWriter, ownerMask); - if (observerMask != 0) Compression.CompressVarUInt(observersWriter, observerMask); + if (ownerMaskReliable != 0) Compression.CompressVarUInt(ownerWriterReliable, ownerMaskReliable); + if (observerMaskReliable != 0) Compression.CompressVarUInt(observersWriterReliable, observerMaskReliable); + + if (ownerMaskUnreliableDelta != 0) Compression.CompressVarUInt(ownerWriterUnreliableDelta, ownerMaskUnreliableDelta); + if (observerMaskUnreliableDelta != 0) Compression.CompressVarUInt(observersWriterUnreliableDelta, observerMaskUnreliableDelta); + + if (ownerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(ownerWriterUnreliableBaseline, ownerMaskUnreliableBaseline); + if (observerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(observersWriterUnreliableBaseline, observerMaskUnreliableBaseline); // serialize all components // perf: only iterate if either dirty mask has dirty bits. - if ((ownerMask | observerMask) != 0) + if ((ownerMaskReliable | observerMaskReliable | + ownerMaskUnreliableBaseline | observerMaskUnreliableBaseline | + ownerMaskUnreliableDelta | observerMaskUnreliableDelta) + != 0) { for (int i = 0; i < components.Length; ++i) { @@ -1063,9 +1197,15 @@ internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter // SyncDirection it's not guaranteed to be in owner anymore. // so we need to serialize to temporary writer first. // and then copy as needed. - bool ownerDirty = IsDirty(ownerMask, i); - bool observersDirty = IsDirty(observerMask, i); - if (ownerDirty || observersDirty) + bool ownerDirtyReliable = IsDirty(ownerMaskReliable, i); + bool observersDirtyReliable = IsDirty(observerMaskReliable, i); + bool ownerDirtyUnreliableBaseline = IsDirty(ownerMaskUnreliableBaseline, i); + bool observersDirtyUnreliableBaseline = IsDirty(observerMaskUnreliableBaseline, i); + bool ownerDirtyUnreliableDelta = IsDirty(ownerMaskUnreliableDelta, i); + bool observersDirtyUnreliableDelta = IsDirty(observerMaskUnreliableDelta, i); + + // RELIABLE COMPONENTS ///////////////////////////////////// + if (ownerDirtyReliable || observersDirtyReliable) { // serialize into helper writer using (NetworkWriterPooled temp = NetworkWriterPool.Get()) @@ -1074,20 +1214,57 @@ internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter ArraySegment segment = temp.ToArraySegment(); // copy to owner / observers as needed - if (ownerDirty) ownerWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); - if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (ownerDirtyReliable) ownerWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirtyReliable) observersWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count); } // dirty bits indicate 'changed since last delta sync'. // clear them after a delta sync here. comp.ClearAllDirtyBits(); } + // UNRELIABLE DELTA //////////////////////////////////////// + // we always send the unreliable delta no matter what + if (ownerDirtyUnreliableDelta || observersDirtyUnreliableDelta) + { + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, false); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirtyUnreliableDelta) ownerWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirtyUnreliableDelta) observersWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count); + + // clear sync time to only send delta again after syncInterval. + comp.lastSyncTime = NetworkTime.localTime; + } + } + // UNRELIABLE BASELINE ///////////////////////////////////// + // sometimes we need the unreliable baseline + // (we always sync deltas, so no 'else if' here) + if (unreliableBaseline && (ownerDirtyUnreliableBaseline || observersDirtyUnreliableBaseline)) + { + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, true); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirtyUnreliableBaseline) ownerWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirtyUnreliableBaseline) observersWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count); + } + + // for unreliable components, only clear dirty bits after the reliable baseline. + // -> don't clear sync time: that's for delta syncs. + comp.ClearAllDirtyBits(false); + } } } } - // serialize components into writer on the client. - internal void SerializeClient(NetworkWriter writer) + // serialize Reliable and Unreliable components in one iteration. + // having two separate functions doing two iterations would be too costly. + internal void SerializeClient(NetworkWriter writerReliable, NetworkWriter writerUnreliableBaseline, NetworkWriter writerUnreliableDelta, bool unreliableBaseline) { // ensure NetworkBehaviours are valid before usage ValidateComponents(); @@ -1100,7 +1277,7 @@ internal void SerializeClient(NetworkWriter writer) // instead of writing a 1 byte index per component, // we limit components to 64 bits and write one ulong instead. // the ulong is also varint compressed for minimum bandwidth. - ulong dirtyMask = ClientDirtyMask(); + ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta); // varint compresses the mask to 1 byte in most cases. // instead of writing an 8 byte ulong. @@ -1111,25 +1288,28 @@ internal void SerializeClient(NetworkWriter writer) // if nothing dirty, then don't even write the mask. // otherwise, every unchanged object would send a 1 byte dirty mask! - if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask); + if (dirtyMaskReliable != 0) Compression.CompressVarUInt(writerReliable, dirtyMaskReliable); + if (dirtyMaskUnreliableDelta != 0) Compression.CompressVarUInt(writerUnreliableDelta, dirtyMaskUnreliableDelta); + if (dirtyMaskUnreliableBaseline != 0) Compression.CompressVarUInt(writerUnreliableBaseline, dirtyMaskUnreliableBaseline); // serialize all components // perf: only iterate if dirty mask has dirty bits. - if (dirtyMask != 0) + if (dirtyMaskReliable != 0 || dirtyMaskUnreliableDelta != 0 || dirtyMaskUnreliableBaseline != 0) { // serialize all components for (int i = 0; i < components.Length; ++i) { NetworkBehaviour comp = components[i]; + // RELIABLE SERIALIZATION ////////////////////////////////// // is this component dirty? // reuse the mask instead of calling comp.IsDirty() again here. - if (IsDirty(dirtyMask, i)) + if (IsDirty(dirtyMaskReliable, i)) // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) { // serialize into writer. // server always knows initialState, we never need to send it - comp.Serialize(writer, false); + comp.Serialize(writerReliable, false); // clear dirty bits for the components that we serialized. // do not clear for _all_ components, only the ones that @@ -1139,13 +1319,39 @@ internal void SerializeClient(NetworkWriter writer) // was elapsed, as then they wouldn't be synced. comp.ClearAllDirtyBits(); } + // UNRELIABLE DELTA //////////////////////////////////////// + // we always send the unreliable delta no matter what + if (IsDirty(dirtyMaskUnreliableDelta, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + comp.Serialize(writerUnreliableDelta, false); + + // clear sync time to only send delta again after syncInterval. + comp.lastSyncTime = NetworkTime.localTime; + } + // UNRELIABLE BASELINE ///////////////////////////////////// + // sometimes we need the unreliable baseline + // (we always sync deltas, so no 'else if' here) + if (unreliableBaseline && IsDirty(dirtyMaskUnreliableBaseline, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + comp.Serialize(writerUnreliableBaseline, true); + + // for unreliable components, only clear dirty bits after the reliable baseline. + // unreliable deltas aren't guaranteed to be delivered, no point in clearing bits. + // -> don't clear sync time: that's for delta syncs. + comp.ClearAllDirtyBits(false); + } + + //////////////////////////////////////////////////////////// } } } // deserialize components from the client on the server. - // there's no 'initialState'. server always knows the initial state. - internal bool DeserializeServer(NetworkReader reader) + // for reliable state sync, server always knows the initial state. + // for unreliable, we always sync full state so we still need the parameter. + internal bool DeserializeServer(NetworkReader reader, bool initialState) { // ensure NetworkBehaviours are valid before usage ValidateComponents(); @@ -1169,7 +1375,7 @@ internal bool DeserializeServer(NetworkReader reader) // deserialize this component // server always knows the initial state (initial=false) // disconnect if failed, to prevent exploits etc. - if (!comp.Deserialize(reader, false)) return false; + if (!comp.Deserialize(reader, initialState)) return false; // server received state from the owner client. // set dirty so it's broadcast to other clients too. @@ -1213,7 +1419,10 @@ internal void DeserializeClient(NetworkReader reader, bool initialState) // get cached serialization for this tick (or serialize if none yet). // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks. // calls SerializeServer, so this function is to be called on server. - internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick) + // + // unreliableBaselineElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + // for reliable components, it just means sync as usual. + internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick, bool unreliableBaselineElapsed) { // only rebuild serialization once per tick. reuse otherwise. // except for tests, where Time.frameCount never increases. @@ -1230,9 +1439,17 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick) // reset lastSerialization.ResetWriters(); - // serialize - SerializeServer_Broadcast(lastSerialization.ownerWriter, - lastSerialization.observersWriter); + // serialize both Reliable and Unreliable components in one iteration. + // doing each in their own iteration would be too costly. + SerializeServer_Broadcast( + lastSerialization.ownerWriterReliable, + lastSerialization.observersWriterReliable, + lastSerialization.ownerWriterUnreliableBaseline, + lastSerialization.observersWriterUnreliableBaseline, + lastSerialization.ownerWriterUnreliableDelta, + lastSerialization.observersWriterUnreliableDelta, + unreliableBaselineElapsed + ); // set tick lastSerialization.tick = tick; diff --git a/Assets/Mirror/Core/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs index 5c9a33c19..708e6b9d9 100644 --- a/Assets/Mirror/Core/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -46,6 +46,12 @@ public class NetworkManager : MonoBehaviour [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; + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + [Tooltip("Send unreliable messages twice to make up for message drops. This doubles bandwidth, but allows for smaller buffer time / faster sync.\nBest to turn this off unless your game is extremely fast paced.")] + public bool unreliableRedundancy = false; + // Deprecated 2023-11-25 // Using SerializeField and HideInInspector to self-correct for being // replaced by headlessStartMode. This can be removed in the future. @@ -318,6 +324,7 @@ void ApplyConfiguration() { NetworkServer.tickRate = sendRate; NetworkServer.unreliableBaselineRate = unreliableBaselineRate; + NetworkServer.unreliableRedundancy = unreliableRedundancy; NetworkClient.snapshotSettings = snapshotSettings; NetworkClient.connectionQualityInterval = evaluationInterval; NetworkClient.connectionQualityMethod = evaluationMethod; diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 937bb6d83..0c29d0692 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -60,6 +60,11 @@ public static partial class NetworkServer public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms static double lastUnreliableBaselineTime; + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + public static bool unreliableRedundancy = false; + /// Connection to host mode client (if any) public static LocalConnectionToClient localConnection { get; private set; } @@ -318,6 +323,8 @@ internal static void RegisterMessageHandlers() RegisterHandler(NetworkTime.OnServerPing, false); RegisterHandler(NetworkTime.OnServerPong, false); RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnEntityStateMessageUnreliableBaseline, true); + RegisterHandler(OnEntityStateMessageUnreliableDelta, true); RegisterHandler(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through } @@ -406,7 +413,10 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta { // DeserializeServer checks permissions internally. // failure to deserialize disconnects to prevent exploits. - if (!identity.DeserializeServer(reader)) + // -> initialState=false because for Reliable messages, + // initial always comes from server and broadcast + // updates are always deltas. + if (!identity.DeserializeServer(reader, false)) { if (exceptionsDisconnect) { @@ -429,6 +439,137 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); } + // for client's owned ClientToServer components. + static void OnEntityStateMessageUnreliableBaseline(NetworkConnectionToClient connection, EntityStateMessageUnreliableBaseline message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Reliable) + { + Debug.LogError($"Server OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!"); + return; + } + + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. + + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + // set the last received reliable baseline tick number. + identity.lastUnreliableBaselineReceived = message.baselineTick; + + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + // + // full state updates (initial=true) arrive over reliable. + if (!identity.DeserializeServer(reader, true)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}."); + } + } + } + // An attacker may attempt to modify another connection's entity + // This could also be a race condition of message in flight when + // RemoveClientAuthority is called, so not malicious. + // Don't disconnect, just log the warning. + else + Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority."); + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + + // for client's owned ClientToServer components. + static void OnEntityStateMessageUnreliableDelta(NetworkConnectionToClient connection, EntityStateMessageUnreliableDelta message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Unreliable) + { + Debug.LogError($"Server OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!"); + return; + } + + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. + + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + // unreliable state sync messages may arrive out of order. + // only ever apply state that's newer than the last received state. + // note that we send one EntityStateMessage per Entity, + // so there will be multiple with the same == timestamp. + if (connection.remoteTimeStamp < identity.lastUnreliableStateTime) + { + // debug log to show that it's working. + // can be tested via LatencySimulation scramble easily. + Debug.Log($"Server caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + // UDP messages may accidentally arrive twice. + // or even intentionally, if unreliableRedundancy is turned on. + else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime) + { + // only log this if unreliableRedundancy is disabled. + // otherwise it's expected and will happen a lot. + if (!unreliableRedundancy) Debug.Log($"Server caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + + // make sure this delta is for the correct baseline. + // we don't want to apply an old delta on top of a new baseline. + if (message.baselineTick != identity.lastUnreliableBaselineReceived) + { + Debug.Log($"Server caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine."); + return; + } + + // set the new last received time for unreliable + identity.lastUnreliableStateTime = connection.remoteTimeStamp; + + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + // + // delta state updates (initial=false) arrive over unreliable. + if (!identity.DeserializeServer(reader, false)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}."); + } + } + } + // An attacker may attempt to modify another connection's entity + // This could also be a race condition of message in flight when + // RemoveClientAuthority is called, so not malicious. + // Don't disconnect, just log the warning. + else + Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority."); + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + // client sends TimeSnapshotMessage every sendInterval. // batching already includes the remoteTimestamp. // we simply insert it on-message here. @@ -1876,11 +2017,18 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize) // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection - static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + // -> unreliableBaselineElapsed: even though we only care about RELIABLE + // components here, GetServerSerializationAtTick still caches all + // the serializations for this frame. and when caching we already + // need to know if the unreliable baseline will be needed or not. + static NetworkWriter SerializeForConnection_ReliableComponents( + NetworkIdentity identity, + NetworkConnectionToClient connection, + bool unreliableBaselineElapsed) { // get serialization for this entity (cached) // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed); // is this entity owned by this connection? bool owned = identity.connectionToClient == connection; @@ -1890,23 +2038,65 @@ static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkCon if (owned) { // was it dirty / did we actually serialize anything? - if (serialization.ownerWriter.Position > 0) - return serialization.ownerWriter; + if (serialization.ownerWriterReliable.Position > 0) + return serialization.ownerWriterReliable; } // observers writer if not owned else { // was it dirty / did we actually serialize anything? - if (serialization.observersWriter.Position > 0) - return serialization.observersWriter; + if (serialization.observersWriterReliable.Position > 0) + return serialization.observersWriterReliable; } // nothing was serialized return null; } + // helper function to get the right serialization for a connection + static (NetworkWriter, NetworkWriter) SerializeForConnection_UnreliableComponents( + NetworkIdentity identity, + NetworkConnectionToClient connection, + bool unreliableBaselineElapsed) + { + // get serialization for this entity (cached) + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed); + + // is this entity owned by this connection? + bool owned = identity.connectionToClient == connection; + + NetworkWriter baselineWriter = null; + NetworkWriter deltaWriter = null; + + // send serialized data + // owner writer if owned + if (owned) + { + // was it dirty / did we actually serialize anything? + if (serialization.ownerWriterUnreliableBaseline.Position > 0) + baselineWriter = serialization.ownerWriterUnreliableBaseline; + + if (serialization.ownerWriterUnreliableDelta.Position > 0) + deltaWriter = serialization.ownerWriterUnreliableDelta; + } + // observers writer if not owned + else + { + // was it dirty / did we actually serialize anything? + if (serialization.observersWriterUnreliableBaseline.Position > 0) + baselineWriter = serialization.observersWriterUnreliableBaseline; + + if (serialization.observersWriterUnreliableDelta.Position > 0) + deltaWriter = serialization.observersWriterUnreliableDelta; + } + + // nothing was serialized + return (baselineWriter, deltaWriter); + } + // helper function to broadcast the world to a connection - static void BroadcastToConnection(NetworkConnectionToClient connection) + static void BroadcastToConnection(NetworkConnectionToClient connection, bool unreliableBaselineElapsed) { // for each entity that this connection is seeing bool hasNull = false; @@ -1918,9 +2108,24 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) // NetworkServer.Destroy) if (identity != null) { + // 'Reliable' sync: send Reliable components over reliable with initial/delta // get serialization for this entity viewed by this connection // (if anything was serialized this time) - NetworkWriter serialization = SerializeForConnection(identity, connection); + NetworkWriter serialization = SerializeForConnection_ReliableComponents(identity, connection, + // IMPORTANT: even for Reliable components we must pass unreliableBaselineElapsed! + // + // consider this (in one frame): + // Serialize Reliable (unreliableBaseline=false) + // GetServerSerializationAtTick (unreliableBaseline=false) + // serializes new, clears dirty bits + // Serialize Unreliable (unreliableBaseline=true) + // GetServerSerializationAtTick (unreliableBaseline=true) + // last.baseline != baseline + // serializes new, which does nothing since dirty bits were already cleared above! + // + // TODO make this less magic in the future. too easy to miss. + unreliableBaselineElapsed); + if (serialization != null) { EntityStateMessage message = new EntityStateMessage @@ -1930,6 +2135,51 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) }; connection.Send(message); } + + // 'Unreliable' sync: send Unreliable components over unreliable + // state is 'initial' for reliable baseline, and 'not initial' for unreliable deltas. + // note that syncInterval is always ignored for unreliable in order to have tick aligned [SyncVars]. + // even if we pass SyncMethod.Reliable, it serializes with initialState=true. + (NetworkWriter baselineSerialization, NetworkWriter deltaSerialization) = SerializeForConnection_UnreliableComponents(identity, connection, unreliableBaselineElapsed); + + // send unreliable delta first. ideally we want this to arrive before the new baseline. + // reliable baseline also clears dirty bits, so unreliable must be sent first. + if (deltaSerialization != null) + { + EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta + { + baselineTick = identity.lastUnreliableBaselineSent, + netId = identity.netId, + payload = deltaSerialization.ToArraySegment() + }; + connection.Send(message, Channels.Unreliable); + + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + if (unreliableRedundancy) connection.Send(message, Channels.Unreliable); + } + + // if it's for a baseline sync, then send a reliable baseline message too. + // this will likely arrive slightly after the unreliable delta above. + if (unreliableBaselineElapsed) + { + if (baselineSerialization != null) + { + // remember last sent baseline tick for this entity. + // (byte) to minimize bandwidth. we don't need the full tick, + // just something small to compare against. + identity.lastUnreliableBaselineSent = (byte)Time.frameCount; + + EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline + { + baselineTick = identity.lastUnreliableBaselineSent, + netId = identity.netId, + payload = baselineSerialization.ToArraySegment() + }; + connection.Send(message, Channels.Reliable); + } + } } // spawned list should have no null entries because we // always call Remove in OnObjectDestroy everywhere. @@ -1968,7 +2218,8 @@ static bool DisconnectIfInactive(NetworkConnectionToClient connection) internal static readonly List connectionsCopy = new List(); - static void Broadcast() + // unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + static void Broadcast(bool unreliableBaselineElapsed) { // copy all connections into a helper collection so that // OnTransportDisconnected can be called while iterating. @@ -2005,7 +2256,7 @@ static void Broadcast() connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); // broadcast world state to this connection - BroadcastToConnection(connection); + BroadcastToConnection(connection, unreliableBaselineElapsed); } // update connection to flush out batched messages @@ -2061,7 +2312,7 @@ internal static void NetworkLateUpdate() bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime); if (!Application.isPlaying || sendIntervalElapsed) - Broadcast(); + Broadcast(unreliableBaselineElapsed); } // process all outgoing messages after updating the world diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index f45742029..ce1442be4 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -101,7 +101,7 @@ protected void DrawDefaultSyncSettings() // 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); + EditorGUILayout.HelpBox("Beware!\nUnreliable is experimental, do not use this yet!", MessageType.Warning); } // sync interval diff --git a/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab b/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab index 7da88c445..8fe05e613 100644 --- a/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab +++ b/Assets/Mirror/Examples/Tanks/Prefabs/Tank.prefab @@ -11,7 +11,6 @@ GameObject: - component: {fileID: 4492442352427800} - component: {fileID: 114118589361100106} - component: {fileID: 114250499875391520} - - component: {fileID: 3464953498043699706} - component: {fileID: 114654712548978148} - component: {fileID: 6900008319038825817} m_Layer: 0 @@ -31,6 +30,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5803173220413450940} - {fileID: 2155495746218491392} @@ -50,7 +50,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 - _assetId: 3454335836 + _assetId: 2638947628 serverOnly: 0 visibility: 0 hasSpawned: 0 @@ -63,9 +63,10 @@ MonoBehaviour: m_GameObject: {fileID: 1916082411674582} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3} + m_Script: {fileID: 11500000, guid: d993bac37a92145448c1ea027b3e9ddc, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 1 syncDirection: 1 syncMode: 0 syncInterval: 0 @@ -73,53 +74,22 @@ MonoBehaviour: syncPosition: 1 syncRotation: 1 syncScale: 0 - onlySyncOnChange: 1 - compressRotation: 1 + onlySyncOnChange: 0 + compressRotation: 0 interpolatePosition: 1 interpolateRotation: 1 interpolateScale: 0 coordinateSpace: 0 - timelineOffset: 0 + timelineOffset: 1 showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - bufferResetMultiplier: 3 - positionSensitivity: 0.01 + onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 - scaleSensitivity: 0.01 ---- !u!114 &3464953498043699706 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1916082411674582} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3} - m_Name: - m_EditorClassIdentifier: - syncDirection: 1 - syncMode: 0 - syncInterval: 0 - target: {fileID: 5803173220413450936} - syncPosition: 0 - syncRotation: 1 - syncScale: 0 - onlySyncOnChange: 1 - compressRotation: 1 - interpolatePosition: 0 - interpolateRotation: 1 - interpolateScale: 0 - coordinateSpace: 0 - timelineOffset: 0 - showGizmos: 0 - showOverlay: 0 - overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - bufferResetMultiplier: 3 - positionSensitivity: 0.01 - rotationSensitivity: 0.01 - scaleSensitivity: 0.01 + positionPrecision: 0.01 + rotationPrecision: 0.001 + scalePrecision: 0.01 + debugDraw: 1 --- !u!114 &114654712548978148 MonoBehaviour: m_ObjectHideFlags: 0 @@ -132,6 +102,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 7deadf756194d461e9140e42d651693b, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 1 syncDirection: 0 syncMode: 0 syncInterval: 0.1 @@ -196,6 +167,7 @@ Transform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 5, z: 0} m_LocalScale: {x: 0.1, y: 0.1, z: 0.1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 4492442352427800} m_RootOrder: 1 @@ -211,10 +183,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -239,6 +213,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!102 &955977906578811009 TextMesh: serializedVersion: 3 @@ -337,9 +312,9 @@ PrefabInstance: objectReference: {fileID: 0} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} ---- !u!4 &5803173220413450940 stripped +--- !u!4 &606281948174800110 stripped Transform: - m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} m_PrefabInstance: {fileID: 7130959241934869977} m_PrefabAsset: {fileID: 0} @@ -355,9 +330,9 @@ Transform: type: 3} m_PrefabInstance: {fileID: 7130959241934869977} m_PrefabAsset: {fileID: 0} ---- !u!4 &606281948174800110 stripped +--- !u!4 &5803173220413450940 stripped Transform: - m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c, + m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} m_PrefabInstance: {fileID: 7130959241934869977} m_PrefabAsset: {fileID: 0} diff --git a/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks.unity b/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks.unity index 92957c3e3..09d1b7df9 100644 --- a/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks.unity +++ b/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks.unity @@ -13,7 +13,7 @@ OcclusionCullingSettings: --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 - serializedVersion: 10 + serializedVersion: 9 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 @@ -44,6 +44,7 @@ RenderSettings: LightmapSettings: m_ObjectHideFlags: 0 serializedVersion: 12 + m_GIWorkflowMode: 1 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -66,6 +67,9 @@ LightmapSettings: 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 @@ -100,7 +104,7 @@ NavMeshSettings: serializedVersion: 2 m_ObjectHideFlags: 0 m_BuildSettings: - serializedVersion: 3 + serializedVersion: 2 agentTypeID: 0 agentRadius: 2 agentHeight: 3.5 @@ -113,7 +117,7 @@ NavMeshSettings: cellSize: 0.6666667 manualTileSize: 0 tileSize: 256 - buildHeightMesh: 0 + accuratePlacement: 0 maxJobWorkers: 0 preserveTilesOutsideBounds: 0 debug: @@ -151,17 +155,9 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 - m_Iso: 200 - m_ShutterSpeed: 0.005 - m_Aperture: 16 - m_FocusDistance: 10 - m_FocalLength: 50 - m_BladeCount: 5 - m_Curvature: {x: 2, y: 11} - m_BarrelClipping: 0.25 - m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -195,13 +191,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 88936773} - serializedVersion: 2 m_LocalRotation: {x: 0.3420201, y: 0, z: 0, w: 0.9396927} m_LocalPosition: {x: 0, y: 20, z: -30} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} + m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 40, y: 0, z: 0} --- !u!114 &88936778 MonoBehaviour: @@ -244,13 +240,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 251893064} - serializedVersion: 2 m_LocalRotation: {x: 0, y: -0.92387956, z: 0, w: 0.38268343} m_LocalPosition: {x: 14, y: 0, z: 14} 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: -135, z: 0} --- !u!114 &251893066 MonoBehaviour: @@ -288,13 +284,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 535739935} - serializedVersion: 2 m_LocalRotation: {x: 0, y: -0.38268343, z: 0, w: 0.92387956} m_LocalPosition: {x: 14, y: 0, z: -14} 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: -45, z: 0} --- !u!114 &535739937 MonoBehaviour: @@ -315,7 +311,8 @@ LightingSettings: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: Settings.lighting - serializedVersion: 8 + serializedVersion: 4 + m_GIWorkflowMode: 1 m_EnableBakedLightmaps: 0 m_EnableRealtimeLightmaps: 0 m_RealtimeEnvironmentLighting: 1 @@ -325,8 +322,6 @@ LightingSettings: m_UsingShadowmask: 1 m_BakeBackend: 1 m_LightmapMaxSize: 1024 - m_LightmapSizeFixed: 0 - m_UseMipmapLimits: 1 m_BakeResolution: 40 m_Padding: 2 m_LightmapCompression: 3 @@ -344,6 +339,9 @@ LightingSettings: m_RealtimeResolution: 2 m_ForceWhiteAlbedo: 0 m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 m_PVRCulling: 1 m_PVRSampling: 1 m_PVRDirectSampleCount: 32 @@ -353,7 +351,7 @@ LightingSettings: m_LightProbeSampleCountMultiplier: 4 m_PVRBounces: 2 m_PVRMinBounces: 2 - m_PVREnvironmentImportanceSampling: 0 + m_PVREnvironmentMIS: 1 m_PVRFilteringMode: 2 m_PVRDenoiserTypeDirect: 0 m_PVRDenoiserTypeIndirect: 0 @@ -367,7 +365,7 @@ LightingSettings: m_PVRFilteringAtrousPositionSigmaDirect: 0.5 m_PVRFilteringAtrousPositionSigmaIndirect: 2 m_PVRFilteringAtrousPositionSigmaAO: 1 - m_RespectSceneVisibilityWhenBakingGI: 0 + m_PVRTiledBaking: 0 --- !u!1 &1107091652 GameObject: m_ObjectHideFlags: 0 @@ -404,8 +402,6 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 m_RayTraceProcedural: 0 - m_RayTracingAccelStructBuildFlagsOverride: 0 - m_RayTracingAccelStructBuildFlags: 1 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -439,17 +435,9 @@ MeshCollider: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1107091652} m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 m_IsTrigger: 0 - m_ProvidesContacts: 0 m_Enabled: 1 - serializedVersion: 5 + serializedVersion: 4 m_Convex: 0 m_CookingOptions: 30 m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} @@ -468,13 +456,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1107091652} - serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 4, y: 1, z: 4} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} + m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1282001517 GameObject: @@ -503,13 +491,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1282001517} - serializedVersion: 2 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 &1282001519 MonoBehaviour: @@ -541,18 +529,22 @@ MonoBehaviour: runInBackground: 1 headlessStartMode: 1 editorAutoStart: 0 - sendRate: 30 + sendRate: 5 + unreliableBaselineRate: 1 + unreliableRedundancy: 0 autoStartServerBuild: 0 autoConnectClientBuild: 0 offlineScene: onlineScene: + offlineSceneLoadDelay: 0 transport: {fileID: 1282001521} networkAddress: localhost maxConnections: 100 disconnectInactiveConnections: 0 disconnectInactiveTimeout: 60 authenticator: {fileID: 0} - playerPrefab: {fileID: 1916082411674582, guid: 6f43bf5488a7443d19ab2a83c6b91f35, type: 3} + playerPrefab: {fileID: 1916082411674582, guid: 6f43bf5488a7443d19ab2a83c6b91f35, + type: 3} autoCreatePlayer: 1 playerSpawnMethod: 1 spawnPrefabs: @@ -641,13 +633,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1458789072} - serializedVersion: 2 m_LocalRotation: {x: 0, y: 0.92387956, z: 0, w: 0.38268343} m_LocalPosition: {x: -14, y: 0, z: 14} 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: 135, z: 0} --- !u!114 &1458789074 MonoBehaviour: @@ -685,13 +677,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1501912662} - serializedVersion: 2 m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} m_LocalPosition: {x: -14, y: 0, z: -14} 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: 45, z: 0} --- !u!114 &1501912664 MonoBehaviour: @@ -730,8 +722,9 @@ Light: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2054208274} m_Enabled: 1 - serializedVersion: 11 + serializedVersion: 10 m_Type: 1 + m_Shape: 0 m_Color: {r: 1, g: 1, b: 1, a: 1} m_Intensity: 1 m_Range: 10 @@ -790,23 +783,11 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2054208274} - serializedVersion: 2 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: 0 m_LocalEulerAnglesHint: {x: 50, y: 150, z: 0} ---- !u!1660057539 &9223372036854775807 -SceneRoots: - m_ObjectHideFlags: 0 - m_Roots: - - {fileID: 2054208276} - - {fileID: 1107091656} - - {fileID: 88936777} - - {fileID: 1282001518} - - {fileID: 251893065} - - {fileID: 535739936} - - {fileID: 1458789073} - - {fileID: 1501912663} diff --git a/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs b/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs index 28550bfcb..99b20832f 100644 --- a/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs +++ b/Assets/Mirror/Examples/Tanks/Scripts/Tank.cs @@ -20,16 +20,24 @@ public class Tank : NetworkBehaviour public Transform projectileMount; [Header("Stats")] - [SyncVar] public int health = 5; + public int health = 5; + int lastHealth = 5; void Update() { + // manual setdirty test + if (health != lastHealth) + { + SetDirty(); + lastHealth = health; + } + // always update health bar. // (SyncVar hook would only update on clients, not on server) healthBar.text = new string('-', health); - + // take input from focused window only - if(!Application.isFocused) return; + if(!Application.isFocused) return; // movement for local player if (isLocalPlayer) @@ -91,5 +99,17 @@ void RotateTurret() turret.transform.LookAt(lookRotation); } } + + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // Debug.LogWarning($"Tank {name} OnSerialize {(initialState ? "full" : "delta")} health={health}"); + writer.WriteInt(health); + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + health = reader.ReadInt(); + // Debug.LogWarning($"Tank {name} OnDeserialize {(initialState ? "full" : "delta")} health={health}"); + } } } diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs index df5fdea67..812dd960a 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs @@ -219,7 +219,7 @@ public void SerializeServer_NotInitial_NotDirty_WritesNothing() // serialize server object. // 'initial' would write everything. // instead, try 'not initial' with 0 dirty bits - serverIdentity.SerializeServer_Broadcast(ownerWriter, observersWriter); + serverIdentity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false); Assert.That(ownerWriter.Position, Is.EqualTo(0)); Assert.That(observersWriter.Position, Is.EqualTo(0)); } @@ -243,7 +243,7 @@ public void SerializeClient_NotInitial_NotDirty_WritesNothing() // clientComp.value = "42"; // serialize client object - clientIdentity.SerializeClient(ownerWriter); + clientIdentity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false); Assert.That(ownerWriter.Position, Is.EqualTo(0)); } @@ -267,7 +267,7 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED() comp2.value = "67890"; // serialize all - identity.SerializeClient(ownerWriter); + identity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false); // shouldn't sync anything. because even though it's ClientToServer, // we don't own this one so we shouldn't serialize & sync it. @@ -302,7 +302,7 @@ public void SerializeServer_OwnerMode_ClientToServer() comp.SetValue(22); // modify with helper function to avoid #3525 ownerWriter.Position = 0; observersWriter.Position = 0; - identity.SerializeServer_Broadcast(ownerWriter, observersWriter); + identity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false); Debug.Log("delta ownerWriter: " + ownerWriter); Debug.Log("delta observersWriter: " + observersWriter); Assert.That(ownerWriter.Position, Is.EqualTo(0)); @@ -340,7 +340,7 @@ public void SerializeServer_ObserversMode_ClientToServer() comp.SetValue(22); // modify with helper function to avoid #3525 ownerWriter.Position = 0; observersWriter.Position = 0; - identity.SerializeServer_Broadcast(ownerWriter, observersWriter); + identity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false); Debug.Log("delta ownerWriter: " + ownerWriter); Debug.Log("delta observersWriter: " + observersWriter); Assert.That(ownerWriter.Position, Is.EqualTo(0)); diff --git a/Assets/Mirror/Tests/Runtime/NetworkIdentityTests.cs b/Assets/Mirror/Tests/Runtime/NetworkIdentityTests.cs index f47870427..644c2b8a4 100644 --- a/Assets/Mirror/Tests/Runtime/NetworkIdentityTests.cs +++ b/Assets/Mirror/Tests/Runtime/NetworkIdentityTests.cs @@ -66,10 +66,10 @@ public IEnumerator TestSerializationWithLargeTimestamps() // 14 * 24 hours per day * 60 minutes per hour * 60 seconds per minute = 14 days // NOTE: change this to 'float' to see the tests fail int tick = 14 * 24 * 60 * 60; - NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick); + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick, false); // advance tick ++tick; - NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick); + NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick, false); // if the serialization has been changed the tickTimeStamp should have moved Assert.That(serialization.tick == serializationNew.tick, Is.False);