diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs index 004b9bdd66..9eb54058a4 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs @@ -160,8 +160,8 @@ public class NetworkConfig // todo: transitional. For the next release, only Snapshot should remain // The booleans allow iterative development and testing in the meantime - public bool UseSnapshotDelta = false; - public bool UseSnapshotSpawn = false; + public bool UseSnapshotDelta { get; } = false; + public bool UseSnapshotSpawn { get; } = false; public const int RttAverageSamples = 5; // number of RTT to keep an average of (plus one) public const int RttWindowSize = 64; // number of slots to use for RTT computations (max number of in-flight packets) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 898a673f53..9e39b3cc7e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -417,7 +417,7 @@ private void Initialize(bool server) SnapshotSystem = null; } - SnapshotSystem = new SnapshotSystem(); + SnapshotSystem = new SnapshotSystem(this); if (server) { diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 3dc87e55bb..c30fe6bcd6 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -234,6 +234,11 @@ public void NetworkShow(ulong clientId) throw new VisibilityChangeException("The object is already visible"); } + if (NetworkManager.NetworkConfig.UseSnapshotSpawn) + { + SnapshotSpawn(clientId); + } + Observers.Add(clientId); NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, OwnerClientId, this); @@ -310,23 +315,30 @@ public void NetworkHide(ulong clientId) } - // Send destroy call Observers.Remove(clientId); - var context = NetworkManager.MessageQueueContainer.EnterInternalCommandContext( - MessageQueueContainer.MessageType.DestroyObject, NetworkChannel.Internal, - new[] { clientId }, NetworkUpdateStage.PostLateUpdate); - if (context != null) + if (NetworkManager.NetworkConfig.UseSnapshotSpawn) { - using (var nonNullContext = (InternalCommandContext)context) + SnapshotDespawn(clientId); + } + else + { + // Send destroy call + var context = NetworkManager.MessageQueueContainer.EnterInternalCommandContext( + MessageQueueContainer.MessageType.DestroyObject, NetworkChannel.Internal, + new[] { clientId }, NetworkUpdateStage.PostLateUpdate); + if (context != null) { - var bufferSizeCapture = new CommandContextSizeCapture(nonNullContext); - bufferSizeCapture.StartMeasureSegment(); + using (var nonNullContext = (InternalCommandContext)context) + { + var bufferSizeCapture = new CommandContextSizeCapture(nonNullContext); + bufferSizeCapture.StartMeasureSegment(); - nonNullContext.NetworkWriter.WriteUInt64Packed(NetworkObjectId); + nonNullContext.NetworkWriter.WriteUInt64Packed(NetworkObjectId); - var size = bufferSizeCapture.StopMeasureSegment(); - NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, NetworkObjectId, name, size); + var size = bufferSizeCapture.StopMeasureSegment(); + NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, NetworkObjectId, name, size); + } } } } @@ -405,6 +417,74 @@ private void OnDestroy() } } + private SnapshotDespawnCommand GetDespawnCommand() + { + SnapshotDespawnCommand command; + command.NetworkObjectId = NetworkObjectId; + command.TickWritten = default; // value will be set internally by SnapshotSystem + command.TargetClientIds = default; + + return command; + } + + private SnapshotSpawnCommand GetSpawnCommand() + { + SnapshotSpawnCommand command; + command.NetworkObjectId = NetworkObjectId; + command.OwnerClientId = OwnerClientId; + command.IsPlayerObject = IsPlayerObject; + command.IsSceneObject = (IsSceneObject == null) || IsSceneObject.Value; + + ulong? parent = NetworkManager.SpawnManager.GetSpawnParentId(this); + if (parent != null) + { + command.ParentNetworkId = parent.Value; + } + else + { + // write own network id, when no parents. todo: optimize this. + command.ParentNetworkId = command.NetworkObjectId; + } + + command.GlobalObjectIdHash = HostCheckForGlobalObjectIdHashOverride(); + // todo: check if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(clientId)) for any clientId + command.ObjectPosition = transform.position; + command.ObjectRotation = transform.rotation; + command.ObjectScale = transform.localScale; + command.TickWritten = default; // value will be set internally by SnapshotSystem + command.TargetClientIds = default; + + return command; + } + + private void SnapshotSpawn() + { + var command = GetSpawnCommand(); + NetworkManager.SnapshotSystem.Spawn(command); + } + + private void SnapshotSpawn(ulong clientId) + { + var command = GetSpawnCommand(); + command.TargetClientIds = new List(); + command.TargetClientIds.Add(clientId); + NetworkManager.SnapshotSystem.Spawn(command); + } + + internal void SnapshotDespawn() + { + var command = GetDespawnCommand(); + NetworkManager.SnapshotSystem.Despawn(command); + } + + internal void SnapshotDespawn(ulong clientId) + { + var command = GetDespawnCommand(); + command.TargetClientIds = new List(); + command.TargetClientIds.Add(clientId); + NetworkManager.SnapshotSystem.Despawn(command); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SpawnInternal(bool destroyWithScene, ulong? ownerClientId, bool playerObject) { @@ -422,32 +502,7 @@ private void SpawnInternal(bool destroyWithScene, ulong? ownerClientId, bool pla if (NetworkManager.NetworkConfig.UseSnapshotSpawn) { - SnapshotSpawnCommand command; - command.NetworkObjectId = NetworkObjectId; - command.OwnerClientId = OwnerClientId; - command.IsPlayerObject = IsPlayerObject; - command.IsSceneObject = (IsSceneObject == null) || IsSceneObject.Value; - - ulong? parent = NetworkManager.SpawnManager.GetSpawnParentId(this); - if (parent != null) - { - command.ParentNetworkId = parent.Value; - } - else - { - // write own network id, when no parents. todo: optimize this. - command.ParentNetworkId = command.NetworkObjectId; - } - - command.GlobalObjectIdHash = HostCheckForGlobalObjectIdHashOverride(); - // todo: check if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(clientId)) for any clientId - command.ObjectPosition = transform.position; - command.ObjectRotation = transform.rotation; - command.ObjectScale = transform.localScale; - command.TickWritten = 0; // will be reset in Spawn - command.TargetClientIds = default; - - NetworkManager.SnapshotSystem.Spawn(command); + SnapshotSpawn(); } ulong ownerId = ownerClientId != null ? ownerClientId.Value : NetworkManager.ServerClientId; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/SnapshotSystem.cs b/com.unity.netcode.gameobjects/Runtime/Core/SnapshotSystem.cs index 937b6501a2..661c06318f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/SnapshotSystem.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/SnapshotSystem.cs @@ -10,21 +10,31 @@ namespace Unity.Netcode // Might include tick in a future milestone, to address past variable value internal struct VariableKey { - public ulong NetworkObjectId; // the NetworkObjectId of the owning GameObject - public ushort BehaviourIndex; // the index of the behaviour in this GameObject - public ushort VariableIndex; // the index of the variable in this NetworkBehaviour - public int TickWritten; // the network tick at which this variable was set + internal ulong NetworkObjectId; // the NetworkObjectId of the owning GameObject + internal ushort BehaviourIndex; // the index of the behaviour in this GameObject + internal ushort VariableIndex; // the index of the variable in this NetworkBehaviour + internal int TickWritten; // the network tick at which this variable was set } // Index for a NetworkVariable in our table of variables // Store when a variable was written and where the variable is serialized internal struct Entry { - public VariableKey Key; - public ushort Position; // the offset in our Buffer - public ushort Length; // the Length of the data in Buffer + internal VariableKey Key; + internal ushort Position; // the offset in our Buffer + internal ushort Length; // the Length of the data in Buffer - public const int NotFound = -1; + internal const int NotFound = -1; + } + + internal struct SnapshotDespawnCommand + { + // identity + internal ulong NetworkObjectId; + + // snapshot internal + internal int TickWritten; + internal List TargetClientIds; } internal struct SnapshotSpawnCommand @@ -44,8 +54,8 @@ internal struct SnapshotSpawnCommand internal Quaternion ObjectRotation; internal Vector3 ObjectScale; - internal ushort TickWritten; - + // snapshot internal + internal int TickWritten; internal List TargetClientIds; } @@ -58,26 +68,31 @@ internal class Snapshot { // todo --M1-- functionality to grow these will be needed in a later milestone private const int k_MaxVariables = 2000; - private const int k_MaxSpawns = 100; + private int m_MaxSpawns = 100; + private int m_MaxDespawns = 100; + private const int k_BufferSize = 30000; - public byte[] MainBuffer = new byte[k_BufferSize]; // buffer holding a snapshot in memory - public byte[] RecvBuffer = new byte[k_BufferSize]; // buffer holding the received snapshot message + internal byte[] MainBuffer = new byte[k_BufferSize]; // buffer holding a snapshot in memory + internal byte[] RecvBuffer = new byte[k_BufferSize]; // buffer holding the received snapshot message internal IndexAllocator Allocator; - public Entry[] Entries = new Entry[k_MaxVariables]; - public int LastEntry = 0; + internal Entry[] Entries = new Entry[k_MaxVariables]; + internal int LastEntry = 0; - public SnapshotSpawnCommand[] Spawns = new SnapshotSpawnCommand[k_MaxSpawns]; - public int NumSpawns = 0; + internal SnapshotSpawnCommand[] Spawns; + internal int NumSpawns = 0; + + internal SnapshotDespawnCommand[] Despawns; + internal int NumDespawns = 0; private MemoryStream m_BufferStream; - private NetworkManager m_NetworkManager; - private bool m_TickIndex; + internal NetworkManager NetworkManager; // indexed by ObjectId - internal Dictionary TickApplied = new Dictionary(); + internal Dictionary TickAppliedSpawn = new Dictionary(); + internal Dictionary TickAppliedDespawn = new Dictionary(); /// /// Constructor @@ -85,16 +100,16 @@ internal class Snapshot /// /// The NetworkManaher this Snapshot uses. Needed upon receive to set Variables /// Whether this Snapshot uses the tick as an index - public Snapshot(NetworkManager networkManager, bool tickIndex) + internal Snapshot() { m_BufferStream = new MemoryStream(RecvBuffer, 0, k_BufferSize); // we ask for twice as many slots because there could end up being one free spot between each pair of slot used Allocator = new IndexAllocator(k_BufferSize, k_MaxVariables * 2); - m_NetworkManager = networkManager; - m_TickIndex = tickIndex; + Spawns = new SnapshotSpawnCommand[m_MaxSpawns]; + Despawns = new SnapshotDespawnCommand[m_MaxDespawns]; } - public void Clear() + internal void Clear() { LastEntry = 0; Allocator.Reset(); @@ -104,7 +119,7 @@ public void Clear() /// Finds the position of a given NetworkVariable, given its key /// /// The key we're looking for - public int Find(VariableKey key) + internal int Find(VariableKey key) { // todo: Add a IEquatable interface for VariableKey. Rely on that instead. for (int i = 0; i < LastEntry; i++) @@ -124,7 +139,7 @@ public int Find(VariableKey key) /// /// Adds an entry in the table for a new key /// - public int AddEntry(in VariableKey k) + internal int AddEntry(in VariableKey k) { var pos = LastEntry++; var entry = Entries[pos]; @@ -137,25 +152,44 @@ public int AddEntry(in VariableKey k) return pos; } - internal void AddSpawn(SnapshotSpawnCommand command) + internal List GetClientList() { - if (NumSpawns < k_MaxSpawns) + List clientList; + clientList = new List(); + + if (!NetworkManager.IsServer) { - command.TargetClientIds = new List(); - if (!m_NetworkManager.IsServer) - { - command.TargetClientIds.Add(m_NetworkManager.ServerClientId); - } - else + clientList.Add(NetworkManager.ServerClientId); + } + else + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) { - foreach (var clientId in m_NetworkManager.ConnectedClientsIds) + if (clientId != NetworkManager.ServerClientId) { - if (clientId != m_NetworkManager.ServerClientId) - { - command.TargetClientIds.Add(clientId); - } + clientList.Add(clientId); } } + } + + return clientList; + } + + internal void AddSpawn(SnapshotSpawnCommand command) + { + if (NumSpawns >= m_MaxSpawns) + { + Array.Resize(ref Spawns, 2 * m_MaxSpawns); + m_MaxSpawns = m_MaxSpawns * 2; + // Debug.Log($"[JEFF] spawn size is now {m_MaxSpawns}"); + } + + if (NumSpawns < m_MaxSpawns) + { + if (command.TargetClientIds == default) + { + command.TargetClientIds = GetClientList(); + } // todo: // this 'if' might be temporary, but is needed to help in debugging @@ -168,6 +202,29 @@ internal void AddSpawn(SnapshotSpawnCommand command) } } + internal void AddDespawn(SnapshotDespawnCommand command) + { + if (NumDespawns >= m_MaxDespawns) + { + Array.Resize(ref Despawns, 2 * m_MaxDespawns); + m_MaxDespawns = m_MaxDespawns * 2; + // Debug.Log($"[JEFF] despawn size is now {m_MaxDespawns}"); + } + + if (NumDespawns < m_MaxDespawns) + { + if (command.TargetClientIds == default) + { + command.TargetClientIds = GetClientList(); + } + if (command.TargetClientIds.Count > 0) + { + Despawns[NumDespawns] = command; + NumDespawns++; + } + } + } + /// /// Write an Entry to send /// Must match ReadEntry @@ -178,7 +235,7 @@ internal void WriteEntry(NetworkWriter writer, in Entry entry) //todo: major refactor. // use blittable types and copy variable in memory locally // only serialize when put on the wire for network transfer - writer.WriteUInt64(entry.Key.NetworkObjectId); + writer.WriteUInt64Packed(entry.Key.NetworkObjectId); writer.WriteUInt16(entry.Key.BehaviourIndex); writer.WriteUInt16(entry.Key.VariableIndex); writer.WriteInt32Packed(entry.Key.TickWritten); @@ -196,20 +253,33 @@ internal void WriteSpawn(ClientData clientData, NetworkWriter writer, in Snapsho s.SequenceNumber = clientData.SequenceNumber; clientData.SentSpawns.Add(s); - writer.WriteUInt64(spawn.NetworkObjectId); - writer.WriteUInt64(spawn.GlobalObjectIdHash); + writer.WriteUInt64Packed(spawn.NetworkObjectId); + writer.WriteUInt64Packed(spawn.GlobalObjectIdHash); writer.WriteBool(spawn.IsSceneObject); writer.WriteBool(spawn.IsPlayerObject); - writer.WriteUInt64(spawn.OwnerClientId); - writer.WriteUInt64(spawn.ParentNetworkId); + writer.WriteUInt64Packed(spawn.OwnerClientId); + writer.WriteUInt64Packed(spawn.ParentNetworkId); writer.WriteVector3(spawn.ObjectPosition); writer.WriteRotation(spawn.ObjectRotation); writer.WriteVector3(spawn.ObjectScale); - writer.WriteUInt16(spawn.TickWritten); + writer.WriteInt32Packed(spawn.TickWritten); } + internal void WriteDespawn(ClientData clientData, NetworkWriter writer, in SnapshotDespawnCommand despawn) + { + // remember which spawn we sent this connection with which sequence number + // that way, upon ack, we can track what is being ack'ed + ClientData.SentSpawn s; + s.ObjectId = despawn.NetworkObjectId; + s.Tick = despawn.TickWritten; + s.SequenceNumber = clientData.SequenceNumber; + clientData.SentSpawns.Add(s); + + writer.WriteUInt64Packed(despawn.NetworkObjectId); + writer.WriteInt32Packed(despawn.TickWritten); + } /// /// Read a received Entry /// Must match WriteEntry @@ -218,7 +288,7 @@ internal void WriteSpawn(ClientData clientData, NetworkWriter writer, in Snapsho internal Entry ReadEntry(NetworkReader reader) { Entry entry; - entry.Key.NetworkObjectId = reader.ReadUInt64(); + entry.Key.NetworkObjectId = reader.ReadUInt64Packed(); entry.Key.BehaviourIndex = reader.ReadUInt16(); entry.Key.VariableIndex = reader.ReadUInt16(); entry.Key.TickWritten = reader.ReadInt32Packed(); @@ -232,29 +302,39 @@ internal SnapshotSpawnCommand ReadSpawn(NetworkReader reader) { SnapshotSpawnCommand command; - command.NetworkObjectId = reader.ReadUInt64(); - command.GlobalObjectIdHash = (uint)reader.ReadUInt64(); + command.NetworkObjectId = reader.ReadUInt64Packed(); + command.GlobalObjectIdHash = (uint)reader.ReadUInt64Packed(); command.IsSceneObject = reader.ReadBool(); command.IsPlayerObject = reader.ReadBool(); - command.OwnerClientId = reader.ReadUInt64(); - command.ParentNetworkId = reader.ReadUInt64(); + command.OwnerClientId = reader.ReadUInt64Packed(); + command.ParentNetworkId = reader.ReadUInt64Packed(); command.ObjectPosition = reader.ReadVector3(); command.ObjectRotation = reader.ReadRotation(); command.ObjectScale = reader.ReadVector3(); - command.TickWritten = reader.ReadUInt16(); + command.TickWritten = reader.ReadInt32Packed(); command.TargetClientIds = default; return command; } + internal SnapshotDespawnCommand ReadDespawn(NetworkReader reader) + { + SnapshotDespawnCommand command; + + command.NetworkObjectId = reader.ReadUInt64Packed(); + command.TickWritten = reader.ReadInt32Packed(); + command.TargetClientIds = default; + + return command; + } /// /// Allocate memory from the buffer for the Entry and update it to point to the right location /// /// The entry to allocate for /// The need size in bytes - public void AllocateEntry(ref Entry entry, int index, int size) + internal void AllocateEntry(ref Entry entry, int index, int size) { // todo: deal with full buffer @@ -340,34 +420,56 @@ internal void ReadIndex(NetworkReader reader) internal void ReadSpawns(NetworkReader reader) { - SnapshotSpawnCommand command; - short count = reader.ReadInt16(); + SnapshotSpawnCommand spawnCommand; + SnapshotDespawnCommand despawnCommand; - for (var i = 0; i < count; i++) + short spawnCount = reader.ReadInt16(); + short despawnCount = reader.ReadInt16(); + + for (var i = 0; i < spawnCount; i++) { - command = ReadSpawn(reader); + spawnCommand = ReadSpawn(reader); - if (TickApplied.ContainsKey(command.NetworkObjectId) && - command.TickWritten <= TickApplied[command.NetworkObjectId]) + if (TickAppliedSpawn.ContainsKey(spawnCommand.NetworkObjectId) && + spawnCommand.TickWritten <= TickAppliedSpawn[spawnCommand.NetworkObjectId]) { continue; } - TickApplied[command.NetworkObjectId] = command.TickWritten; + TickAppliedSpawn[spawnCommand.NetworkObjectId] = spawnCommand.TickWritten; + + // Debug.Log($"[Spawn] {spawnCommand.NetworkObjectId} {spawnCommand.TickWritten}"); - // what is a soft sync ? - // what are spawn payloads ? - if (command.ParentNetworkId == command.NetworkObjectId) + if (spawnCommand.ParentNetworkId == spawnCommand.NetworkObjectId) { - var networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false, command.GlobalObjectIdHash, command.OwnerClientId, null, command.ObjectPosition, command.ObjectRotation); - m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, command.NetworkObjectId, true, command.IsPlayerObject, command.OwnerClientId, null, false, false); + var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(false, spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, null, spawnCommand.ObjectPosition, spawnCommand.ObjectRotation); + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, null, false, false); } else { - var networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false, command.GlobalObjectIdHash, command.OwnerClientId, command.ParentNetworkId, command.ObjectPosition, command.ObjectRotation); - m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, command.NetworkObjectId, true, command.IsPlayerObject, command.OwnerClientId, null, false, false); + var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(false, spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, spawnCommand.ParentNetworkId, spawnCommand.ObjectPosition, spawnCommand.ObjectRotation); + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, null, false, false); } } + for (var i = 0; i < despawnCount; i++) + { + despawnCommand = ReadDespawn(reader); + + if (TickAppliedDespawn.ContainsKey(despawnCommand.NetworkObjectId) && + despawnCommand.TickWritten <= TickAppliedDespawn[despawnCommand.NetworkObjectId]) + { + continue; + } + + TickAppliedDespawn[despawnCommand.NetworkObjectId] = despawnCommand.TickWritten; + + // Debug.Log($"[DeSpawn] {despawnCommand.NetworkObjectId} {despawnCommand.TickWritten}"); + + NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(despawnCommand.NetworkObjectId, + out NetworkObject networkObject); + + NetworkManager.SpawnManager.OnDespawnObject(networkObject, true); + } } internal ushort ReadAcks(ulong clientId, ClientData clientData, NetworkReader reader) @@ -390,8 +492,8 @@ internal ushort ReadAcks(ulong clientId, ClientData clientData, NetworkReader re clientData.SpawnAck[sent.ObjectId] = sent.Tick; } - // check the spawn commands, find it, and if this is the last connection - // to ack, let's remove it + // check the spawn and despawn commands, find them, and if this is the last connection + // to ack, let's remove them for (var i = 0; i < NumSpawns; i++) { if (Spawns[i].TickWritten == sent.Tick && @@ -408,6 +510,22 @@ internal ushort ReadAcks(ulong clientId, ClientData clientData, NetworkReader re } } } + for (var i = 0; i < NumDespawns; i++) + { + if (Despawns[i].TickWritten == sent.Tick && + Despawns[i].NetworkObjectId == sent.ObjectId) + { + Despawns[i].TargetClientIds.Remove(clientId); + + if (Despawns[i].TargetClientIds.Count == 0) + { + // remove by moving the last spawn over + Despawns[i] = Despawns[NumDespawns - 1]; + NumDespawns--; + break; + } + } + } } } @@ -421,7 +539,7 @@ internal ushort ReadAcks(ulong clientId, ClientData clientData, NetworkReader re /// The key to search for private INetworkVariable FindNetworkVar(VariableKey key) { - var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects; + var spawnedObjects = NetworkManager.SpawnManager.SpawnedObjects; if (spawnedObjects.ContainsKey(key.NetworkObjectId)) { @@ -437,7 +555,7 @@ private INetworkVariable FindNetworkVar(VariableKey key) internal class ClientData { - internal struct SentSpawn + internal struct SentSpawn // this struct also stores Despawns, not just Spawns { internal ulong SequenceNumber; internal ulong ObjectId; @@ -447,23 +565,28 @@ internal struct SentSpawn internal ushort SequenceNumber = 0; // the next sequence number to use for this client internal ushort LastReceivedSequence = 0; // the last sequence number received by this client + internal int NextSpawnIndex = 0; // index of the last spawn sent. Used to cycle through spawns (LRU scheme) + internal int NextDespawnIndex = 0; // same as above, but for despawns. + // by objectId - // which spawns did this connection ack'ed ? + // which spawns and despawns did this connection ack'ed ? internal Dictionary SpawnAck = new Dictionary(); - // list of spawn commands we sent, with sequence number + // list of spawn and despawns commands we sent, with sequence number // need to manage acknowledgements internal List SentSpawns = new List(); } - public class SnapshotSystem : INetworkUpdateSystem, IDisposable + internal class SnapshotSystem : INetworkUpdateSystem, IDisposable { // temporary, debugging sentinels internal const ushort SentinelBefore = 0x4246; internal const ushort SentinelAfter = 0x89CE; - private NetworkManager m_NetworkManager = NetworkManager.Singleton; - private Snapshot m_Snapshot = new Snapshot(NetworkManager.Singleton, false); + private const int k_MaxSpawnUsage = 1000; // max bytes to use for the spawn/despawn part + + private NetworkManager m_NetworkManager = default; + private Snapshot m_Snapshot = default; // by clientId private Dictionary m_ClientData = new Dictionary(); @@ -471,6 +594,20 @@ public class SnapshotSystem : INetworkUpdateSystem, IDisposable private int m_CurrentTick = NetworkTickSystem.NoTick; + /// + /// Constructor + /// + /// Registers the snapshot system for early updates, keeps reference to the NetworkManager + internal SnapshotSystem(NetworkManager networkManager) + { + m_Snapshot = new Snapshot(); + + m_NetworkManager = networkManager; + m_Snapshot.NetworkManager = networkManager; + + this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); + } + internal ConnectionRtt GetConnectionRtt(ulong clientId) { if (!m_ConnectionRtts.ContainsKey(clientId)) @@ -481,15 +618,6 @@ internal ConnectionRtt GetConnectionRtt(ulong clientId) return m_ConnectionRtts[clientId]; } - /// - /// Constructor - /// - /// Registers the snapshot system for early updates - public SnapshotSystem() - { - this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); - } - /// /// Dispose /// @@ -501,8 +629,6 @@ public void Dispose() public void NetworkUpdate(NetworkUpdateStage updateStage) { - //TODO: here, make sure we check both path, vars and spawns. - if (!m_NetworkManager.NetworkConfig.UseSnapshotDelta && !m_NetworkManager.NetworkConfig.UseSnapshotSpawn) { return; @@ -595,42 +721,84 @@ private void SendSnapshot(ulong clientId) private void WriteSpawns(NetworkBuffer buffer, ulong clientId) { var spawnWritten = 0; + var despawnWritten = 0; + + bool overSize = false; + ClientData clientData = m_ClientData[clientId]; + + // this is needed because spawns being removed may have reduce the size below LRU position + if (m_Snapshot.NumSpawns > 0) + { + clientData.NextSpawnIndex %= m_Snapshot.NumSpawns; + } + else + { + clientData.NextSpawnIndex = 0; + } + + if (m_Snapshot.NumDespawns > 0) + { + clientData.NextDespawnIndex %= m_Snapshot.NumDespawns; + } + else + { + clientData.NextDespawnIndex = 0; + } using (var writer = PooledNetworkWriter.Get(buffer)) { - var positionBefore = writer.GetStream().Position; + var positionSpawns = writer.GetStream().Position; writer.WriteInt16((short)m_Snapshot.NumSpawns); + var positionDespawns = writer.GetStream().Position; + writer.WriteInt16((short)m_Snapshot.NumDespawns); - for (var i = 0; i < m_Snapshot.NumSpawns; i++) + for (var j = 0; j < m_Snapshot.NumSpawns && !overSize; j++) { - bool skip = false; + var index = clientData.NextSpawnIndex; - // todo : check that this condition is the same as the clientId one, then remove id :-) - if (m_ClientData[clientId].SpawnAck.ContainsKey(m_Snapshot.Spawns[i].NetworkObjectId)) + if (m_Snapshot.Spawns[index].TargetClientIds.Contains(clientId)) { - if (m_ClientData[clientId].SpawnAck[m_Snapshot.Spawns[i].NetworkObjectId] == - m_Snapshot.Spawns[i].TickWritten) - { - skip = true; - } + m_Snapshot.WriteSpawn(clientData, writer, in m_Snapshot.Spawns[index]); + spawnWritten++; } - if (!m_Snapshot.Spawns[i].TargetClientIds.Contains(clientId)) + // limit spawn sizes, compare current pos to very first position we wrote to + if (writer.GetStream().Position - positionSpawns > k_MaxSpawnUsage) { - skip = true; + overSize = true; } + clientData.NextSpawnIndex = (clientData.NextSpawnIndex + 1) % m_Snapshot.NumSpawns; + } + + + for (var j = 0; j < m_Snapshot.NumDespawns && !overSize; j++) + { + var index = clientData.NextDespawnIndex; - if (!skip) + if (m_Snapshot.Despawns[index].TargetClientIds.Contains(clientId)) { - m_Snapshot.WriteSpawn(m_ClientData[clientId], writer, in m_Snapshot.Spawns[i]); - spawnWritten++; + m_Snapshot.WriteDespawn(clientData, writer, in m_Snapshot.Despawns[index]); + despawnWritten++; + } + // limit spawn sizes, compare current pos to very first position we wrote to + if (writer.GetStream().Position - positionSpawns > k_MaxSpawnUsage) + { + overSize = true; } + clientData.NextDespawnIndex = (clientData.NextDespawnIndex + 1) % m_Snapshot.NumDespawns; } - var positionAfter = writer.GetStream().Position; - writer.GetStream().Position = positionBefore; + long positionAfter = 0; + + positionAfter = writer.GetStream().Position; + writer.GetStream().Position = positionSpawns; writer.WriteInt16((short)spawnWritten); writer.GetStream().Position = positionAfter; + + positionAfter = writer.GetStream().Position; + writer.GetStream().Position = positionDespawns; + writer.WriteInt16((short)despawnWritten); + writer.GetStream().Position = positionAfter; } } @@ -678,8 +846,18 @@ private void WriteBuffer(NetworkBuffer buffer) internal void Spawn(SnapshotSpawnCommand command) { - command.TickWritten = (ushort)m_CurrentTick; + command.TickWritten = m_CurrentTick; m_Snapshot.AddSpawn(command); + + // Debug.Log($"[Spawn] {command.NetworkObjectId} {command.TickWritten}"); + } + + internal void Despawn(SnapshotDespawnCommand command) + { + command.TickWritten = m_CurrentTick; + m_Snapshot.AddDespawn(command); + + // Debug.Log($"[DeSpawn] {command.NetworkObjectId} {command.TickWritten}"); } // todo: consider using a Key, instead of 3 ints, if it can be exposed @@ -688,7 +866,7 @@ internal void Spawn(SnapshotSpawnCommand command) /// Might not happen for all variable on every frame. Might even happen more than once. /// /// The NetworkVariable to write, or rather, its INetworkVariable - public void Store(ulong networkObjectId, int behaviourIndex, int variableIndex, INetworkVariable networkVariable) + internal void Store(ulong networkObjectId, int behaviourIndex, int variableIndex, INetworkVariable networkVariable) { VariableKey k; k.NetworkObjectId = networkObjectId; @@ -731,8 +909,14 @@ private void WriteVariableToSnapshot(Snapshot snapshot, INetworkVariable network /// /// /// The stream to read from - public void ReadSnapshot(ulong clientId, Stream snapshotStream) + internal void ReadSnapshot(ulong clientId, Stream snapshotStream) { + // poor man packet loss simulation + //if (Random.Range(0, 10) > 5) + //{ + // return; + //} + // todo: temporary hack around bug if (!m_NetworkManager.IsServer) { @@ -758,7 +942,7 @@ public void ReadSnapshot(ulong clientId, Stream snapshotStream) var sentinel = reader.ReadUInt16(); if (sentinel != SentinelBefore) { - Debug.Log("JEFF Critical : snapshot integrity (before)"); + Debug.Log("Critical : snapshot integrity (before)"); } m_Snapshot.ReadBuffer(reader, snapshotStream); @@ -767,12 +951,12 @@ public void ReadSnapshot(ulong clientId, Stream snapshotStream) var ackSequence = m_Snapshot.ReadAcks(clientId, m_ClientData[clientId], reader); // keep track of RTTs, using the sequence number acknowledgement as a marker - m_ConnectionRtts[clientId].NotifyAck(ackSequence, Time.unscaledTime); + GetConnectionRtt(clientId).NotifyAck(ackSequence, Time.unscaledTime); sentinel = reader.ReadUInt16(); if (sentinel != SentinelAfter) { - Debug.Log("JEFF Critical : snapshot integrity (after)"); + Debug.Log("Critical : snapshot integrity (after)"); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 500a9d8c17..0af4537cd3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -654,30 +654,37 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec }); } - var messageQueueContainer = NetworkManager.MessageQueueContainer; - if (messageQueueContainer != null) + if (NetworkManager.NetworkConfig.UseSnapshotSpawn) + { + networkObject.SnapshotDespawn(); + } + else { - if (networkObject != null) + var messageQueueContainer = NetworkManager.MessageQueueContainer; + if (messageQueueContainer != null) { - // As long as we have any remaining clients, then notify of the object being destroy. - if (NetworkManager.ConnectedClientsList.Count > 0) + if (networkObject != null) { - - ulong[] clientIds = NetworkManager.ConnectedClientsIds; - var context = messageQueueContainer.EnterInternalCommandContext( - MessageQueueContainer.MessageType.DestroyObject, NetworkChannel.Internal, - clientIds, NetworkUpdateStage.PostLateUpdate); - if (context != null) + // As long as we have any remaining clients, then notify of the object being destroy. + if (NetworkManager.ConnectedClientsList.Count > 0) { - using (var nonNullContext = (InternalCommandContext)context) + + ulong[] clientIds = NetworkManager.ConnectedClientsIds; + var context = messageQueueContainer.EnterInternalCommandContext( + MessageQueueContainer.MessageType.DestroyObject, NetworkChannel.Internal, + clientIds, NetworkUpdateStage.PostLateUpdate); + if (context != null) { - var bufferSizeCapture = new CommandContextSizeCapture(nonNullContext); - bufferSizeCapture.StartMeasureSegment(); + using (var nonNullContext = (InternalCommandContext)context) + { + var bufferSizeCapture = new CommandContextSizeCapture(nonNullContext); + bufferSizeCapture.StartMeasureSegment(); - nonNullContext.NetworkWriter.WriteUInt64Packed(networkObject.NetworkObjectId); + nonNullContext.NetworkWriter.WriteUInt64Packed(networkObject.NetworkObjectId); - var size = bufferSizeCapture.StopMeasureSegment(); - NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientIds, networkObject.NetworkObjectId, networkObject.name, size); + var size = bufferSizeCapture.StopMeasureSegment(); + NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientIds, networkObject.NetworkObjectId, networkObject.name, size); + } } } } diff --git a/com.unity.netcode.gameobjects/Tests/Editor/SnapshotRttTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/SnapshotRttTests.cs index 3134a6613f..bb3aa7b5a4 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/SnapshotRttTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/SnapshotRttTests.cs @@ -9,7 +9,7 @@ public class SnapshotRttTests [Test] public void TestBasicRtt() { - var snapshot = new SnapshotSystem(); + var snapshot = new SnapshotSystem(default); var client1 = snapshot.GetConnectionRtt(0); client1.NotifySend(0, 0.0); @@ -40,7 +40,7 @@ public void TestBasicRtt() [Test] public void TestEdgeCasesRtt() { - var snapshot = new SnapshotSystem(); + var snapshot = new SnapshotSystem(NetworkManager.Singleton); var client1 = snapshot.GetConnectionRtt(0); var iterationCount = NetworkConfig.RttWindowSize * 3; var extraCount = NetworkConfig.RttWindowSize * 2; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs index 70c00db0db..0908243ab5 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -54,8 +55,28 @@ public GameObject PreparePrefab(Type type) } // Check that the first client see them, or not, as expected - private void CheckVisible(bool target) + private IEnumerator CheckVisible(bool target) { + int count = 0; + do + { + yield return new WaitForSeconds(0.1f); + count++; + + if (count > 20) + { + // timeout waiting for object to reach the expect visibility + Assert.Fail("timeout waiting for object to reach the expect visibility"); + break; + } + } while (m_NetSpawnedObject1.IsNetworkVisibleTo(m_ClientId0) != target || + m_NetSpawnedObject2.IsNetworkVisibleTo(m_ClientId0) != target || + m_NetSpawnedObject3.IsNetworkVisibleTo(m_ClientId0) != target || + m_Object1OnClient0.IsSpawned != target || + m_Object2OnClient0.IsSpawned != target || + m_Object3OnClient0.IsSpawned != target + ); + Debug.Assert(m_NetSpawnedObject1.IsNetworkVisibleTo(m_ClientId0) == target); Debug.Assert(m_NetSpawnedObject2.IsNetworkVisibleTo(m_ClientId0) == target); Debug.Assert(m_NetSpawnedObject3.IsNetworkVisibleTo(m_ClientId0) == target); @@ -66,20 +87,38 @@ private void CheckVisible(bool target) } // Set the 3 objects visibility - private void Show(bool visibility) + private void Show(bool individually, bool visibility) { - var list = new List(); - list.Add(m_NetSpawnedObject1); - list.Add(m_NetSpawnedObject2); - list.Add(m_NetSpawnedObject3); - - if (!visibility) + if (individually) { - NetworkObject.NetworkHide(list, m_ClientId0); + if (!visibility) + { + m_NetSpawnedObject1.NetworkHide(m_ClientId0); + m_NetSpawnedObject2.NetworkHide(m_ClientId0); + m_NetSpawnedObject3.NetworkHide(m_ClientId0); + } + else + { + m_NetSpawnedObject1.NetworkShow(m_ClientId0); + m_NetSpawnedObject2.NetworkShow(m_ClientId0); + m_NetSpawnedObject3.NetworkShow(m_ClientId0); + } } else { - NetworkObject.NetworkShow(list, m_ClientId0); + var list = new List(); + list.Add(m_NetSpawnedObject1); + list.Add(m_NetSpawnedObject2); + list.Add(m_NetSpawnedObject3); + + if (!visibility) + { + NetworkObject.NetworkHide(list, m_ClientId0); + } + else + { + NetworkObject.NetworkShow(list, m_ClientId0); + } } } @@ -134,28 +173,28 @@ public IEnumerator NetworkShowHideTest() m_NetSpawnedObject2.Spawn(); m_NetSpawnedObject3.Spawn(); - yield return new WaitForSeconds(0.1f); - // get the NetworkObject on a client instance - yield return RefreshNetworkObjects(); + for (int mode = 0; mode < 2; mode++) + { + // get the NetworkObject on a client instance + yield return RefreshNetworkObjects(); - // check object start visible - CheckVisible(true); + // check object start visible + yield return CheckVisible(true); - // hide them on one client - Show(false); - yield return new WaitForSeconds(0.1f); + // hide them on one client + Show(mode == 0, false); - // verify they got hidden - CheckVisible(false); + // verify they got hidden + yield return CheckVisible(false); - // show them to that client - Show(true); - yield return new WaitForSeconds(0.1f); - yield return RefreshNetworkObjects(); + // show them to that client + Show(mode == 0, true); + yield return RefreshNetworkObjects(); - // verify they become visible - CheckVisible(true); + // verify they become visible + yield return CheckVisible(true); + } } } }