diff --git a/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs b/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs
index 3222f27f78..f41709d6d2 100644
--- a/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs
+++ b/com.unity.multiplayer.mlapi/Runtime/Configuration/NetworkConstants.cs
@@ -24,6 +24,7 @@ internal static class NetworkConstants
internal const byte NAMED_MESSAGE = 22;
internal const byte SERVER_LOG = 23;
internal const byte SNAPSHOT_DATA = 25;
+ internal const byte SNAPSHOT_ACK = 26;
internal const byte SERVER_RPC = 30;
internal const byte CLIENT_RPC = 31;
internal const byte INVALID = 32;
@@ -56,7 +57,7 @@ internal static class NetworkConstants
"SERVER_LOG",
"",
"SNAPSHOT_DATA",
- "",
+ "SNAPSHOT_ACK",
"",
"",
"",
diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs
new file mode 100644
index 0000000000..0d3a76849a
--- /dev/null
+++ b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs
@@ -0,0 +1,388 @@
+using UnityEngine;
+
+namespace MLAPI
+{
+ internal struct IndexAllocatorEntry
+ {
+ internal int Pos; // Position where the memory of this slot is
+ internal int Length; // Length of the memory allocated to this slot
+ internal int Next; // Next and Prev define the order of the slots in the buffer
+ internal int Prev;
+ internal bool Free; // Whether this is a free slot
+ }
+
+ internal class IndexAllocator
+ {
+ private const int k_NotSet = -1;
+ private readonly int m_MaxSlot; // Maximum number of sections (free or not) in the buffer
+ private readonly int m_BufferSize; // Size of the buffer we allocated into
+ private int m_LastSlot = 0; // Last allocated slot
+ private IndexAllocatorEntry[] m_Slots; // Array of slots
+ private int[] m_IndexToSlot; // Mapping from the client's index to the slot index
+
+ internal IndexAllocator(int bufferSize, int maxSlot)
+ {
+ m_MaxSlot = maxSlot;
+ m_BufferSize = bufferSize;
+ m_Slots = new IndexAllocatorEntry[m_MaxSlot];
+ m_IndexToSlot = new int[m_MaxSlot];
+ Reset();
+ }
+
+ ///
+ /// Reset this IndexAllocator to an empty one, with the same sized buffer and slots
+ ///
+ internal void Reset()
+ {
+ // todo: could be made faster, for example by having a last index
+ // and not needing valid stuff past it
+ for (int i = 0; i < m_MaxSlot; i++)
+ {
+ m_Slots[i].Free = true;
+ m_Slots[i].Next = i + 1;
+ m_Slots[i].Prev = i - 1;
+ m_Slots[i].Pos = m_BufferSize;
+ m_Slots[i].Length = 0;
+
+ m_IndexToSlot[i] = k_NotSet;
+ }
+
+ m_Slots[0].Pos = 0;
+ m_Slots[0].Length = m_BufferSize;
+ m_Slots[0].Prev = k_NotSet;
+ m_Slots[m_MaxSlot - 1].Next = k_NotSet;
+ }
+
+ ///
+ /// Returns the amount of memory used
+ ///
+ ///
+ /// Returns the amount of memory used, starting at 0, ending after the last used slot
+ ///
+ internal int Range
+ {
+ get
+ {
+ // when the whole buffer is free, m_LastSlot points to an empty slot
+ if (m_Slots[m_LastSlot].Free)
+ {
+ return 0;
+ }
+ // otherwise return the end of the last slot used
+ return m_Slots[m_LastSlot].Pos + m_Slots[m_LastSlot].Length;
+ }
+ }
+
+ ///
+ /// Allocate a slot with "size" position, for index "index"
+ ///
+ /// The client index to identify this. Used in Deallocate to identify which slot
+ /// The size required.
+ /// Returns the position to use in the buffer
+ ///
+ /// true if successful, false is there isn't enough memory available or no slots are large enough
+ ///
+ internal bool Allocate(int index, int size, out int pos)
+ {
+ pos = 0;
+ // size must be positive, index must be within range
+ if (size < 0 || index < 0 || index >= m_MaxSlot)
+ {
+ return false;
+ }
+
+ // refuse allocation if the index is already in use
+ if (m_IndexToSlot[index] != k_NotSet)
+ {
+ return false;
+ }
+
+ // todo: this is the slowest part
+ // improvement 1: list of free blocks (minor)
+ // improvement 2: heap of free blocks
+ for (int i = 0; i < m_MaxSlot; i++)
+ {
+ if (m_Slots[i].Free && m_Slots[i].Length >= size)
+ {
+ m_IndexToSlot[index] = i;
+
+ int leftOver = m_Slots[i].Length - size;
+ int next = m_Slots[i].Next;
+ if (m_Slots[next].Free)
+ {
+ m_Slots[next].Pos -= leftOver;
+ m_Slots[next].Length += leftOver;
+ }
+ else
+ {
+ int add = MoveSlotAfter(i);
+
+ m_Slots[add].Pos = m_Slots[i].Pos + size;
+ m_Slots[add].Length = m_Slots[i].Length - size;
+ }
+
+ m_Slots[i].Free = false;
+ m_Slots[i].Length = size;
+
+ pos = m_Slots[i].Pos;
+
+ // if we allocate past the current range, we are the last slot
+ if (m_Slots[i].Pos + m_Slots[i].Length > Range)
+ {
+ m_LastSlot = i;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Deallocate a slot
+ ///
+ /// The client index to identify this. Same index used in Allocate
+ ///
+ /// true if successful, false is there isn't an allocated slot at this index
+ ///
+ internal bool Deallocate(int index)
+ {
+ // size must be positive, index must be within range
+ if (index < 0 || index >= m_MaxSlot)
+ {
+ return false;
+ }
+
+ int slot = m_IndexToSlot[index];
+
+ if (slot == k_NotSet)
+ {
+ return false;
+ }
+
+ if (m_Slots[slot].Free)
+ {
+ return false;
+ }
+
+ m_Slots[slot].Free = true;
+
+ int prev = m_Slots[slot].Prev;
+ int next = m_Slots[slot].Next;
+
+ // if previous slot was free, merge and grow
+ if (prev != k_NotSet && m_Slots[prev].Free)
+ {
+ m_Slots[prev].Length += m_Slots[slot].Length;
+ m_Slots[slot].Length = 0;
+
+ // if the slot we're merging was the last one, the last one is now the one we merged with
+ if (slot == m_LastSlot)
+ {
+ m_LastSlot = prev;
+ }
+
+ // todo: verify what this does on full or nearly full cases
+ MoveSlotToEnd(slot);
+ slot = prev;
+ }
+
+ next = m_Slots[slot].Next;
+
+ // merge with next slot if it is free
+ if (next != k_NotSet && m_Slots[next].Free)
+ {
+ m_Slots[slot].Length += m_Slots[next].Length;
+ m_Slots[next].Length = 0;
+ MoveSlotToEnd(next);
+ }
+
+ // if we just deallocate the last one, we need to move last back
+ if (slot == m_LastSlot)
+ {
+ m_LastSlot = m_Slots[m_LastSlot].Prev;
+ // if there's nothing allocated anymore, use 0
+ if (m_LastSlot == k_NotSet)
+ {
+ m_LastSlot = 0;
+ }
+ }
+
+ // mark the index as available
+ m_IndexToSlot[index] = k_NotSet;
+
+ return true;
+ }
+
+ // Take a slot at the end and link it to go just after "slot".
+ // Used when allocating part of a slot and we need an entry for the rest
+ // Returns the slot that was picked
+ private int MoveSlotAfter(int slot)
+ {
+ int ret = m_Slots[m_MaxSlot - 1].Prev;
+ int p0 = m_Slots[ret].Prev;
+
+ m_Slots[p0].Next = m_MaxSlot - 1;
+ m_Slots[m_MaxSlot - 1].Prev = p0;
+
+ int p1 = m_Slots[slot].Next;
+ m_Slots[slot].Next = ret;
+ m_Slots[p1].Prev = ret;
+
+ m_Slots[ret].Prev = slot;
+ m_Slots[ret].Next = p1;
+
+ return ret;
+ }
+
+ // Move the slot "slot" to the end of the list.
+ // Used when merging two slots, that gives us an extra entry at the end
+ private void MoveSlotToEnd(int slot)
+ {
+ // if we're already there
+ if (m_Slots[slot].Next == k_NotSet)
+ {
+ return;
+ }
+
+ int prev = m_Slots[slot].Prev;
+ int next = m_Slots[slot].Next;
+
+ m_Slots[prev].Next = next;
+ if (next != k_NotSet)
+ {
+ m_Slots[next].Prev = prev;
+ }
+
+ int p0 = m_Slots[m_MaxSlot - 1].Prev;
+
+ m_Slots[p0].Next = slot;
+ m_Slots[slot].Next = m_MaxSlot - 1;
+
+ m_Slots[m_MaxSlot - 1].Prev = slot;
+ m_Slots[slot].Prev = p0;
+
+ m_Slots[slot].Pos = m_BufferSize;
+ }
+
+ // runs a bunch of consistency check on the Allocator
+ internal bool Verify()
+ {
+ int pos = k_NotSet;
+ int count = 0;
+ int total = 0;
+ int endPos = 0;
+
+ do
+ {
+ int prev = pos;
+ if (pos != k_NotSet)
+ {
+ pos = m_Slots[pos].Next;
+ if (pos == k_NotSet)
+ {
+ break;
+ }
+ }
+ else
+ {
+ pos = 0;
+ }
+
+ if (m_Slots[pos].Prev != prev)
+ {
+ // the previous is not correct
+ return false;
+ }
+
+ if (m_Slots[pos].Length < 0)
+ {
+ // Length should be positive
+ return false;
+ }
+
+ if (prev != k_NotSet && m_Slots[prev].Free && m_Slots[pos].Free && m_Slots[pos].Length > 0)
+ {
+ // should not have two consecutive free slots
+ return false;
+ }
+
+ if (m_Slots[pos].Pos != total)
+ {
+ // slots should all line up nicely
+ return false;
+ }
+
+ if (!m_Slots[pos].Free)
+ {
+ endPos = m_Slots[pos].Pos + m_Slots[pos].Length;
+ }
+
+ total += m_Slots[pos].Length;
+ count++;
+
+ } while (pos != k_NotSet);
+
+ if (count != m_MaxSlot)
+ {
+ // some slots were lost
+ return false;
+ }
+
+ if (total != m_BufferSize)
+ {
+ // total buffer should be accounted for
+ return false;
+ }
+
+ if (endPos != Range)
+ {
+ // end position should match reported end position
+ return false;
+ }
+
+ return true;
+ }
+
+ // Debug display the allocator structure
+ internal void DebugDisplay()
+ {
+ string logMessage = "IndexAllocator structure\n";
+
+ bool[] seen = new bool[m_MaxSlot];
+
+ int pos = 0;
+ int count = 0;
+ bool prevEmpty = false;
+ do
+ {
+ seen[pos] = true;
+ count++;
+ if (m_Slots[pos].Length == 0 && prevEmpty)
+ {
+ // don't display repetitive empty slots
+ }
+ else
+ {
+ logMessage += string.Format("{0}:{1}, {2} ({3}) \n", m_Slots[pos].Pos, m_Slots[pos].Length,
+ m_Slots[pos].Free ? "Free" : "Used", pos);
+ if (m_Slots[pos].Length == 0)
+ {
+ prevEmpty = true;
+ }
+ else
+ {
+ prevEmpty = false;
+ }
+ }
+
+ pos = m_Slots[pos].Next;
+ } while (pos != k_NotSet && !seen[pos]);
+
+ logMessage += string.Format("{0} Total entries\n", count);
+
+ Debug.Log(logMessage);
+ }
+ }
+}
diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs.meta b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs.meta
new file mode 100644
index 0000000000..b7c7632344
--- /dev/null
+++ b/com.unity.multiplayer.mlapi/Runtime/Core/IndexAllocator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bd9e1475e8c8e4a6d935fe2409e3bd26
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs
index e37d1cf57a..e5ac693a5f 100644
--- a/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs
+++ b/com.unity.multiplayer.mlapi/Runtime/Core/NetworkManager.cs
@@ -1214,6 +1214,10 @@ internal void HandleIncomingData(ulong clientId, NetworkChannel networkChannel,
case NetworkConstants.SNAPSHOT_DATA:
InternalMessageHandler.HandleSnapshot(clientId, messageStream);
break;
+ case NetworkConstants.SNAPSHOT_ACK:
+ InternalMessageHandler.HandleAck(clientId, messageStream);
+ break;
+
case NetworkConstants.CONNECTION_REQUEST:
if (IsServer)
{
diff --git a/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs b/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs
index 263f515460..216ab945d0 100644
--- a/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs
+++ b/com.unity.multiplayer.mlapi/Runtime/Core/SnapshotSystem.cs
@@ -20,6 +20,7 @@ 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 ushort TickWritten; // the network tick at which this variable was set
}
// Index for a NetworkVariable in our table of variables
@@ -27,9 +28,8 @@ internal struct VariableKey
internal struct Entry
{
public VariableKey Key;
- public ushort TickWritten; // the network tick at which this variable was set
public ushort Position; // the offset in our Buffer
- public ushort Length; // the length of the data in Buffer
+ public ushort Length; // the Length of the data in Buffer
public bool Fresh; // indicates entries that were just received
public const int NotFound = -1;
@@ -43,23 +43,38 @@ internal struct Entry
internal class Snapshot
{
// todo --M1-- functionality to grow these will be needed in a later milestone
- private const int k_MaxVariables = 64;
- private const int k_BufferSize = 20000;
+ private const int k_MaxVariables = 2000;
+ private const int k_BufferSize = 30000;
public byte[] Buffer = new byte[k_BufferSize];
- public int FreeMemoryPosition = 0;
+ internal IndexAllocator Allocator;
public Entry[] Entries = new Entry[k_MaxVariables];
public int LastEntry = 0;
public MemoryStream Stream;
+ private NetworkManager m_NetworkManager;
+ private bool m_TickIndex;
+
///
/// Constructor
/// Allocated a MemoryStream to be reused for this Snapshot
///
- public 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)
{
Stream = new MemoryStream(Buffer, 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;
+ }
+
+ public void Clear()
+ {
+ LastEntry = 0;
+ Allocator.Reset();
}
// todo --M1--
@@ -70,11 +85,13 @@ public Snapshot()
/// The key we're looking for
public int Find(VariableKey key)
{
+ // todo: Add a IEquatable interface for VariableKey. Rely on that instead.
for (int i = 0; i < LastEntry; i++)
{
if (Entries[i].Key.NetworkObjectId == key.NetworkObjectId &&
Entries[i].Key.BehaviourIndex == key.BehaviourIndex &&
- Entries[i].Key.VariableIndex == key.VariableIndex)
+ Entries[i].Key.VariableIndex == key.VariableIndex &&
+ (!m_TickIndex || (Entries[i].Key.TickWritten == key.TickWritten)))
{
return i;
}
@@ -86,16 +103,12 @@ public int Find(VariableKey key)
///
/// Adds an entry in the table for a new key
///
- // todo: change the params to take a Key instead
- public int AddEntry(ulong networkObjectId, int behaviourIndex, int variableIndex)
+ public int AddEntry(in VariableKey k)
{
var pos = LastEntry++;
var entry = Entries[pos];
- entry.Key.NetworkObjectId = networkObjectId;
- entry.Key.BehaviourIndex = (ushort)behaviourIndex;
- entry.Key.VariableIndex = (ushort)variableIndex;
- entry.TickWritten = 0;
+ entry.Key = k;
entry.Position = 0;
entry.Length = 0;
entry.Fresh = false;
@@ -104,29 +117,166 @@ public int AddEntry(ulong networkObjectId, int behaviourIndex, int variableIndex
return pos;
}
+ ///
+ /// Write an Entry to send
+ /// Must match ReadEntry
+ ///
+ /// The writer to write the entry to
+ 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.WriteUInt16(entry.Key.BehaviourIndex);
+ writer.WriteUInt16(entry.Key.VariableIndex);
+ writer.WriteUInt16(entry.Key.TickWritten);
+ writer.WriteUInt16(entry.Position);
+ writer.WriteUInt16(entry.Length);
+ }
+
+ ///
+ /// Read a received Entry
+ /// Must match WriteEntry
+ ///
+ /// The readed to read the entry from
+ internal Entry ReadEntry(NetworkReader reader)
+ {
+ Entry entry;
+ entry.Key.NetworkObjectId = reader.ReadUInt64();
+ entry.Key.BehaviourIndex = reader.ReadUInt16();
+ entry.Key.VariableIndex = reader.ReadUInt16();
+ entry.Key.TickWritten = reader.ReadUInt16();
+ entry.Position = reader.ReadUInt16();
+ entry.Length = reader.ReadUInt16();
+ entry.Fresh = false;
+
+ return entry;
+ }
+
///
/// 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, long size)
+ public void AllocateEntry(ref Entry entry, int index, int size)
{
// todo --M1--
// this will change once we start reusing the snapshot buffer memory
// todo: deal with free space
// todo: deal with full buffer
- entry.Position = (ushort)FreeMemoryPosition;
+ if (entry.Length > 0)
+ {
+ Allocator.Deallocate(index);
+ }
+
+ int pos;
+ bool ret = Allocator.Allocate(index, size, out pos);
+
+ if (!ret)
+ {
+ //todo: error handling
+ }
+
+ entry.Position = (ushort)pos;
entry.Length = (ushort)size;
- FreeMemoryPosition += (int)size;
+ }
+
+ ///
+ /// Read the buffer part of a snapshot
+ /// Must match WriteBuffer
+ /// The stream is actually a memory stream and we seek to each variable position as we deserialize them
+ ///
+ /// The NetworkReader to read our buffer of variables from
+ /// The stream to read our buffer of variables from
+ internal void ReadBuffer(NetworkReader reader, Stream snapshotStream)
+ {
+ int snapshotSize = reader.ReadUInt16();
+
+ snapshotStream.Read(Buffer, 0, snapshotSize);
+
+ for (var i = 0; i < LastEntry; i++)
+ {
+ if (Entries[i].Fresh && Entries[i].Key.TickWritten > 0)
+ {
+ // todo: there might be a race condition here with object reuse. To investigate.
+ var networkVariable = FindNetworkVar(Entries[i].Key);
+
+ if (networkVariable != null)
+ {
+ Stream.Seek(Entries[i].Position, SeekOrigin.Begin);
+
+ // todo: consider refactoring out in its own function to accomodate
+ // other ways to (de)serialize
+ // todo --M1--
+ // Review whether tick still belong in netvar or in the snapshot table.
+ networkVariable.ReadDelta(Stream, m_NetworkManager.IsServer);
+ }
+ }
+
+ Entries[i].Fresh = false;
+ }
+ }
+
+ ///
+ /// Read the snapshot index from a buffer
+ /// Stores the entry. Allocates memory if needed. The actual buffer will be read later
+ ///
+ /// The reader to read the index from
+ internal void ReadIndex(NetworkReader reader)
+ {
+ Entry entry;
+ short entries = reader.ReadInt16();
+
+ for (var i = 0; i < entries; i++)
+ {
+ entry = ReadEntry(reader);
+ entry.Fresh = true;
+
+ int pos = Find(entry.Key);
+ if (pos == Entry.NotFound)
+ {
+ pos = AddEntry(entry.Key);
+ }
+
+ // if we need to allocate more memory (the variable grew in size)
+ if (Entries[pos].Length < entry.Length)
+ {
+ AllocateEntry(ref entry, pos, entry.Length);
+ }
+
+ Entries[pos] = entry;
+ }
+ }
+
+ ///
+ /// Helper function to find the NetworkVariable object from a key
+ /// This will look into all spawned objects
+ ///
+ /// The key to search for
+ private INetworkVariable FindNetworkVar(VariableKey key)
+ {
+ var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects;
+
+ if (spawnedObjects.ContainsKey(key.NetworkObjectId))
+ {
+ var behaviour = spawnedObjects[key.NetworkObjectId]
+ .GetNetworkBehaviourAtOrderIndex(key.BehaviourIndex);
+ return behaviour.NetworkVariableFields[key.VariableIndex];
+ }
+
+ return null;
}
}
public class SnapshotSystem : INetworkUpdateSystem, IDisposable
{
- private Snapshot m_Snapshot = new Snapshot();
- private Snapshot m_ReceivedSnapshot = new Snapshot();
private NetworkManager m_NetworkManager = NetworkManager.Singleton;
+ private Snapshot m_Snapshot = new Snapshot(NetworkManager.Singleton, false);
+ private Dictionary m_ClientReceivedSnapshot = new Dictionary();
+
+ private ushort m_CurrentTick = 0;
///
/// Constructor
@@ -155,21 +305,37 @@ public void NetworkUpdate(NetworkUpdateStage updateStage)
if (updateStage == NetworkUpdateStage.EarlyUpdate)
{
- if (m_NetworkManager.IsServer)
+ var tick = m_NetworkManager.NetworkTickSystem.GetTick();
+
+ if (tick != m_CurrentTick)
{
- for (int i = 0; i < m_NetworkManager.ConnectedClientsList.Count; i++)
+ m_CurrentTick = tick;
+ if (m_NetworkManager.IsServer)
{
- var clientId = m_NetworkManager.ConnectedClientsList[i].ClientId;
- SendSnapshot(clientId);
+ for (int i = 0; i < m_NetworkManager.ConnectedClientsList.Count; i++)
+ {
+ var clientId = m_NetworkManager.ConnectedClientsList[i].ClientId;
+ SendSnapshot(clientId);
+ }
}
- }
- else if (m_NetworkManager.IsConnectedClient)
- {
- SendSnapshot(m_NetworkManager.ServerClientId);
- }
+ else if (m_NetworkManager.IsConnectedClient)
+ {
+ SendSnapshot(m_NetworkManager.ServerClientId);
+ }
+
+ //m_Snapshot.Allocator.DebugDisplay();
+ /*
+ DebugDisplayStore(m_Snapshot, "Entries");
- // DebugDisplayStore(m_Snapshot, "Entries");
- // DebugDisplayStore(m_ReceivedSnapshot, "Received Entries");
+ foreach(var item in m_ClientReceivedSnapshot)
+ {
+ DebugDisplayStore(item.Value, "Received Entries " + item.Key);
+ }
+ */
+ // todo: --M1b--
+ // for now we clear our send snapshot because we don't have per-client partial sends
+ m_Snapshot.Clear();
+ }
}
}
@@ -185,12 +351,16 @@ private void SendSnapshot(ulong clientId)
// Send the entry index and the buffer where the variables are serialized
using (var buffer = PooledNetworkBuffer.Get())
{
+ using (var writer = PooledNetworkWriter.Get(buffer))
+ {
+ writer.WriteUInt16(m_CurrentTick);
+ }
+
WriteIndex(buffer);
WriteBuffer(buffer);
m_NetworkManager.MessageSender.Send(clientId, NetworkConstants.SNAPSHOT_DATA,
NetworkChannel.SnapshotExchange, buffer);
- buffer.Dispose();
}
}
@@ -205,77 +375,11 @@ private void WriteIndex(NetworkBuffer buffer)
writer.WriteInt16((short)m_Snapshot.LastEntry);
for (var i = 0; i < m_Snapshot.LastEntry; i++)
{
- WriteEntry(writer, in m_Snapshot.Entries[i]);
- }
- }
- }
-
- ///
- /// Read the snapshot index from a buffer
- /// Stores the entry. Allocates memory if needed. The actual buffer will be read later
- ///
- /// The reader to read the index from
- private void ReadIndex(NetworkReader reader)
- {
- Entry entry;
- short entries = reader.ReadInt16();
-
- for (var i = 0; i < entries; i++)
- {
- entry = ReadEntry(reader);
- entry.Fresh = true;
-
- int pos = m_ReceivedSnapshot.Find(entry.Key);
- if (pos == Entry.NotFound)
- {
- pos = m_ReceivedSnapshot.AddEntry(entry.Key.NetworkObjectId, entry.Key.BehaviourIndex,
- entry.Key.VariableIndex);
- }
-
- // if we need to allocate more memory (the variable grew in size)
- if (m_ReceivedSnapshot.Entries[pos].Length < entry.Length)
- {
- m_ReceivedSnapshot.AllocateEntry(ref entry, entry.Length);
+ m_Snapshot.WriteEntry(writer, in m_Snapshot.Entries[i]);
}
-
- m_ReceivedSnapshot.Entries[pos] = entry;
}
}
- ///
- /// Write an Entry to send
- /// Must match ReadEntry
- ///
- /// The writer to write the entry to
- private void WriteEntry(NetworkWriter writer, in Entry entry)
- {
- writer.WriteUInt64(entry.Key.NetworkObjectId);
- writer.WriteUInt16(entry.Key.BehaviourIndex);
- writer.WriteUInt16(entry.Key.VariableIndex);
- writer.WriteUInt16(entry.TickWritten);
- writer.WriteUInt16(entry.Position);
- writer.WriteUInt16(entry.Length);
- }
-
- ///
- /// Read a received Entry
- /// Must match WriteEntry
- ///
- /// The readed to read the entry from
- private Entry ReadEntry(NetworkReader reader)
- {
- Entry entry;
- entry.Key.NetworkObjectId = reader.ReadUInt64();
- entry.Key.BehaviourIndex = reader.ReadUInt16();
- entry.Key.VariableIndex = reader.ReadUInt16();
- entry.TickWritten = reader.ReadUInt16();
- entry.Position = reader.ReadUInt16();
- entry.Length = reader.ReadUInt16();
- entry.Fresh = false;
-
- return entry;
- }
-
///
/// Write the buffer of a snapshot
/// Must match ReadBuffer
@@ -285,43 +389,13 @@ private void WriteBuffer(NetworkBuffer buffer)
{
using (var writer = PooledNetworkWriter.Get(buffer))
{
- writer.WriteUInt16((ushort)m_Snapshot.FreeMemoryPosition);
+ writer.WriteUInt16((ushort)m_Snapshot.Allocator.Range);
}
// todo --M1--
// // this sends the whole buffer
// we'll need to build a per-client list
- buffer.Write(m_Snapshot.Buffer, 0, m_Snapshot.FreeMemoryPosition);
- }
-
- ///
- /// Read the buffer part of a snapshot
- /// Must match WriteBuffer
- /// The stream is actually a memory stream and we seek to each variable position as we deserialize them
- ///
- /// The NetworkReader to read our buffer of variables from
- /// The stream to read our buffer of variables from
- private void ReadBuffer(NetworkReader reader, Stream snapshotStream)
- {
- int snapshotSize = reader.ReadUInt16();
-
- snapshotStream.Read(m_ReceivedSnapshot.Buffer, 0, snapshotSize);
-
- for (var i = 0; i < m_ReceivedSnapshot.LastEntry; i++)
- {
- if (m_ReceivedSnapshot.Entries[i].Fresh && m_ReceivedSnapshot.Entries[i].TickWritten > 0)
- {
- var nv = FindNetworkVar(m_ReceivedSnapshot.Entries[i].Key);
-
- m_ReceivedSnapshot.Stream.Seek(m_ReceivedSnapshot.Entries[i].Position, SeekOrigin.Begin);
-
- // todo --M1--
- // Review whether tick still belong in netvar or in the snapshot table.
- nv.ReadDelta(m_ReceivedSnapshot.Stream, m_NetworkManager.IsServer);
- }
-
- m_ReceivedSnapshot.Entries[i].Fresh = false;
- }
+ buffer.Write(m_Snapshot.Buffer, 0, m_Snapshot.Allocator.Range);
}
// todo: consider using a Key, instead of 3 ints, if it can be exposed
@@ -336,60 +410,88 @@ public void Store(ulong networkObjectId, int behaviourIndex, int variableIndex,
k.NetworkObjectId = networkObjectId;
k.BehaviourIndex = (ushort)behaviourIndex;
k.VariableIndex = (ushort)variableIndex;
+ k.TickWritten = m_NetworkManager.NetworkTickSystem.GetTick();
int pos = m_Snapshot.Find(k);
if (pos == Entry.NotFound)
{
- pos = m_Snapshot.AddEntry(networkObjectId, behaviourIndex, variableIndex);
+ pos = m_Snapshot.AddEntry(k);
}
- // write var into buffer, possibly adjusting entry's position and length
+ WriteVariableToSnapshot(m_Snapshot, networkVariable, pos);
+ }
+
+ private void WriteVariableToSnapshot(Snapshot snapshot, INetworkVariable networkVariable, int index)
+ {
+ // write var into buffer, possibly adjusting entry's position and Length
using (var varBuffer = PooledNetworkBuffer.Get())
{
networkVariable.WriteDelta(varBuffer);
- if (varBuffer.Length > m_Snapshot.Entries[pos].Length)
+ if (varBuffer.Length > snapshot.Entries[index].Length)
{
// allocate this Entry's buffer
- m_Snapshot.AllocateEntry(ref m_Snapshot.Entries[pos], varBuffer.Length);
+ snapshot.AllocateEntry(ref snapshot.Entries[index], index, (int)varBuffer.Length);
}
- m_Snapshot.Entries[pos].TickWritten = m_NetworkManager.NetworkTickSystem.GetTick();
// Copy the serialized NetworkVariable into our buffer
- Buffer.BlockCopy(varBuffer.GetBuffer(), 0, m_Snapshot.Buffer, m_Snapshot.Entries[pos].Position, (int)varBuffer.Length);
+ Buffer.BlockCopy(varBuffer.GetBuffer(), 0, snapshot.Buffer, snapshot.Entries[index].Position, (int)varBuffer.Length);
}
}
+
///
/// Entry point when a Snapshot is received
/// This is where we read and store the received snapshot
///
+ ///
/// The stream to read from
- public void ReadSnapshot(Stream snapshotStream)
+ public void ReadSnapshot(ulong clientId, Stream snapshotStream)
{
+ ushort snapshotTick = default;
+
using (var reader = PooledNetworkReader.Get(snapshotStream))
{
- ReadIndex(reader);
- ReadBuffer(reader, snapshotStream);
+ snapshotTick = reader.ReadUInt16();
+
+ if (!m_ClientReceivedSnapshot.ContainsKey(clientId))
+ {
+ m_ClientReceivedSnapshot[clientId] = new Snapshot(m_NetworkManager, false);
+ }
+ var snapshot = m_ClientReceivedSnapshot[clientId];
+
+ // todo --M1b-- temporary, clear before receive.
+ snapshot.Clear();
+
+ snapshot.ReadIndex(reader);
+ snapshot.ReadBuffer(reader, snapshotStream);
}
+
+ SendAck(clientId, snapshotTick);
}
- ///
- /// Helper function to find the NetworkVariable object from a key
- /// This will look into all spawned objects
- ///
- /// The key to search for
- private INetworkVariable FindNetworkVar(VariableKey key)
+ public void ReadAck(ulong clientId, Stream snapshotStream)
{
- var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects;
+ using (var reader = PooledNetworkReader.Get(snapshotStream))
+ {
+ var ackTick = reader.ReadUInt16();
+ //Debug.Log(string.Format("Receive ack {0} from client {1}", ackTick, clientId));
+ }
+ }
- if (spawnedObjects.ContainsKey(key.NetworkObjectId))
+ public void SendAck(ulong clientId, ushort tick)
+ {
+ using (var buffer = PooledNetworkBuffer.Get())
{
- var behaviour = spawnedObjects[key.NetworkObjectId]
- .GetNetworkBehaviourAtOrderIndex(key.BehaviourIndex);
- return behaviour.NetworkVariableFields[key.VariableIndex];
+ using (var writer = PooledNetworkWriter.Get(buffer))
+ {
+ writer.WriteUInt16(tick);
+ }
+
+ m_NetworkManager.MessageSender.Send(clientId, NetworkConstants.SNAPSHOT_ACK,
+ NetworkChannel.SnapshotExchange, buffer);
+ buffer.Dispose();
}
- return null;
}
// todo --M1--
@@ -400,8 +502,8 @@ private void DebugDisplayStore(Snapshot block, string name)
string table = "=== Snapshot table === " + name + " ===\n";
for (int i = 0; i < block.LastEntry; i++)
{
- table += string.Format("NetworkObject {0}:{1}:{2} range [{3}, {4}] ", block.Entries[i].Key.NetworkObjectId, block.Entries[i].Key.BehaviourIndex,
- block.Entries[i].Key.VariableIndex, block.Entries[i].Position, block.Entries[i].Position + block.Entries[i].Length);
+ table += string.Format("NetworkVariable {0}:{1}:{2} written {5}, range [{3}, {4}] ", block.Entries[i].Key.NetworkObjectId, block.Entries[i].Key.BehaviourIndex,
+ block.Entries[i].Key.VariableIndex, block.Entries[i].Position, block.Entries[i].Position + block.Entries[i].Length, block.Entries[i].Key.TickWritten);
for (int j = 0; j < block.Entries[i].Length && j < 4; j++)
{
diff --git a/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs b/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs
index 09fa658c5a..8bc3e25996 100644
--- a/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs
+++ b/com.unity.multiplayer.mlapi/Runtime/Messaging/InternalMessageHandler.cs
@@ -398,7 +398,12 @@ public void HandleNetworkLog(ulong clientId, Stream stream)
internal static void HandleSnapshot(ulong clientId, Stream messageStream)
{
- NetworkManager.Singleton.SnapshotSystem.ReadSnapshot(messageStream);
+ NetworkManager.Singleton.SnapshotSystem.ReadSnapshot(clientId, messageStream);
+ }
+
+ internal static void HandleAck(ulong clientId, Stream messageStream)
+ {
+ NetworkManager.Singleton.SnapshotSystem.ReadAck(clientId, messageStream);
}
public void HandleAllClientsSwitchSceneCompleted(ulong clientId, Stream stream)
diff --git a/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs b/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs
index 53685f95e6..b7cf3c4a00 100644
--- a/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs
+++ b/com.unity.multiplayer.mlapi/Runtime/Transports/NetworkTransport.cs
@@ -104,7 +104,7 @@ public TransportChannel[] MLAPI_CHANNELS
// todo: Currently, fragmentation support needed to deal with oversize packets encounterable with current pre-snapshot code".
// todo: once we have snapshotting able to deal with missing frame, this should be unreliable
new TransportChannel(NetworkChannel.NetworkVariable, NetworkDelivery.ReliableFragmentedSequenced),
- new TransportChannel(NetworkChannel.SnapshotExchange, NetworkDelivery.Unreliable),
+ new TransportChannel(NetworkChannel.SnapshotExchange, NetworkDelivery.ReliableFragmentedSequenced), // todo: temporary until we separate snapshots in chunks
};
///
diff --git a/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs
new file mode 100644
index 0000000000..da5610b235
--- /dev/null
+++ b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs
@@ -0,0 +1,117 @@
+using NUnit.Framework;
+using UnityEngine;
+
+namespace MLAPI.EditorTests
+{
+ public class FixedAllocatorTest
+ {
+ [Test]
+ public void SimpleTest()
+ {
+ int pos;
+
+ var allocator = new IndexAllocator(20000, 200);
+ allocator.DebugDisplay();
+
+ // allocate 20 bytes
+ Assert.IsTrue(allocator.Allocate(0, 20, out pos));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // can't ask for negative amount of memory
+ Assert.IsFalse(allocator.Allocate(1, -20, out pos));
+ Assert.IsTrue(allocator.Verify());
+
+ // can't ask for deallocation of negative index
+ Assert.IsFalse(allocator.Deallocate(-1));
+ Assert.IsTrue(allocator.Verify());
+
+ // can't ask for the same index twice
+ Assert.IsFalse(allocator.Allocate(0, 20, out pos));
+ Assert.IsTrue(allocator.Verify());
+
+ // allocate another 20 bytes
+ Assert.IsTrue(allocator.Allocate(1, 20, out pos));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // allocate a third 20 bytes
+ Assert.IsTrue(allocator.Allocate(2, 20, out pos));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // deallocate 0
+ Assert.IsTrue(allocator.Deallocate(0));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // deallocate 1
+ allocator.Deallocate(1);
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // deallocate 2
+ allocator.Deallocate(2);
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // allocate 50 bytes
+ Assert.IsTrue(allocator.Allocate(0, 50, out pos));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // allocate another 50 bytes
+ Assert.IsTrue(allocator.Allocate(1, 50, out pos));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // allocate a third 50 bytes
+ Assert.IsTrue(allocator.Allocate(2, 50, out pos));
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // deallocate 1, a block in the middle this time
+ allocator.Deallocate(1);
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+
+ // allocate a smaller one in its place
+ allocator.Allocate(1, 25, out pos);
+ allocator.DebugDisplay();
+ Assert.IsTrue(allocator.Verify());
+ }
+
+ [Test]
+ public void ReuseTest()
+ {
+ int count = 100;
+ bool[] used = new bool[count];
+ int[] pos = new int[count];
+ int bufferSize = 20000;
+ int iterations = 10000;
+
+ var allocator = new IndexAllocator(20000, 200);
+
+ for (int i = 0; i < iterations; i++)
+ {
+ int index = Random.Range(0, count);
+ if (used[index])
+ {
+ Assert.IsTrue(allocator.Deallocate(index));
+ used[index] = false;
+ }
+ else
+ {
+ int position;
+ int length = 10 * Random.Range(1, 10);
+ Assert.IsTrue(allocator.Allocate(index, length, out position));
+ pos[index] = position;
+ used[index] = true;
+ }
+ Assert.IsTrue(allocator.Verify());
+ }
+ allocator.DebugDisplay();
+ }
+
+ }
+}
diff --git a/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs.meta b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs.meta
new file mode 100644
index 0000000000..760a4ccfe5
--- /dev/null
+++ b/com.unity.multiplayer.mlapi/Tests/Editor/IndexAllocatorTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 85ac488e1432d49668c711fa625a0743
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: