com.unity.netcode.gameobjects@1.0.0-pre.7

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [1.0.0-pre.7] - 2022-04-01

### Added

- Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828)
- Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823)
- Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762)
- `UnityTransport` settings can now be set programmatically. (#1845)
- `FastBufferWriter` and Reader IsInitialized property. (#1859)

### Changed

- Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849)

### Removed

- Removed `SnapshotSystem` (#1852)
- Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812)
- Removed `com.unity.collections` dependency from the package (#1849)

### Fixed
- Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850)
- Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847)
- Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841)
- Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838)
- Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836)
- Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828)
- Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821)
- Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811)
- Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809)
- Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808)
- Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801)
- Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780)
- Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779)
- Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777)
- Fixed parenting warning printing for false positives (#1855)
This commit is contained in:
Unity Technologies
2022-04-01 00:00:00 +00:00
parent 5b4aaa8b59
commit 60e2dabef4
123 changed files with 5751 additions and 3419 deletions

View File

@@ -11,4 +11,3 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")]
[assembly: InternalsVisibleTo("Unity.Netcode.TestHelpers.Runtime")]
[assembly: InternalsVisibleTo("Unity.Netcode.Adapter.UTP")]

View File

@@ -138,19 +138,6 @@ namespace Unity.Netcode
/// </summary>
public bool EnableNetworkLogs = true;
/// <summary>
/// Whether or not to enable Snapshot System for variable updates. Not supported in this version.
/// </summary>
public bool UseSnapshotDelta { get; internal set; } = false;
/// <summary>
/// Whether or not to enable Snapshot System for spawn and despawn commands. Not supported in this version.
/// </summary>
public bool UseSnapshotSpawn { get; internal set; } = false;
/// <summary>
/// When Snapshot System spawn is enabled: max size of Snapshot Messages. Meant to fit MTU.
/// </summary>
public int SnapshotMaxSpawnUsage { get; } = 1000;
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)
/// <summary>

View File

@@ -20,6 +20,17 @@ namespace Unity.Netcode
/// <summary>
/// The NetworkObject's owned by this Client
/// </summary>
public readonly List<NetworkObject> OwnedObjects = new List<NetworkObject>();
public List<NetworkObject> OwnedObjects
{
get
{
if (PlayerObject != null && PlayerObject.NetworkManager != null && PlayerObject.NetworkManager.IsListening)
{
return PlayerObject.NetworkManager.SpawnManager.GetClientOwnedObjects(ClientId);
}
return new List<NetworkObject>();
}
}
}
}

View File

@@ -1,388 +0,0 @@
using UnityEngine;
namespace Unity.Netcode
{
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();
}
/// <summary>
/// Reset this IndexAllocator to an empty one, with the same sized buffer and slots
/// </summary>
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;
}
/// <summary>
/// Returns the amount of memory used
/// </summary>
/// <returns>
/// Returns the amount of memory used, starting at 0, ending after the last used slot
/// </returns>
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;
}
}
/// <summary>
/// Allocate a slot with "size" position, for index "index"
/// </summary>
/// <param name="index">The client index to identify this. Used in Deallocate to identify which slot</param>
/// <param name="size">The size required. </param>
/// <param name="pos">Returns the position to use in the buffer </param>
/// <returns>
/// true if successful, false is there isn't enough memory available or no slots are large enough
/// </returns>
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;
}
/// <summary>
/// Deallocate a slot
/// </summary>
/// <param name="index">The client index to identify this. Same index used in Allocate </param>
/// <returns>
/// true if successful, false is there isn't an allocated slot at this index
/// </returns>
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);
}
}
}

View File

@@ -158,37 +158,57 @@ namespace Unity.Netcode
// We check to see if we need to shortcut for the case where we are the host/server and we can send a clientRPC
// to ourself. Sadly we have to figure that out from the list of clientIds :(
bool shouldSendToHost = false;
if (clientRpcParams.Send.TargetClientIds != null)
{
foreach (var clientId in clientRpcParams.Send.TargetClientIds)
foreach (var targetClientId in clientRpcParams.Send.TargetClientIds)
{
if (clientId == NetworkManager.ServerClientId)
if (targetClientId == NetworkManager.ServerClientId)
{
shouldSendToHost = true;
break;
}
// Check to make sure we are sending to only observers, if not log an error.
if (NetworkManager.LogLevel >= LogLevel.Error && !NetworkObject.Observers.Contains(targetClientId))
{
NetworkLog.LogError(GenerateObserverErrorMessage(clientRpcParams, targetClientId));
}
}
rpcWriteSize = NetworkManager.SendMessage(ref clientRpcMessage, networkDelivery, in clientRpcParams.Send.TargetClientIds);
}
else if (clientRpcParams.Send.TargetClientIdsNativeArray != null)
{
foreach (var clientId in clientRpcParams.Send.TargetClientIdsNativeArray)
foreach (var targetClientId in clientRpcParams.Send.TargetClientIdsNativeArray)
{
if (clientId == NetworkManager.ServerClientId)
if (targetClientId == NetworkManager.ServerClientId)
{
shouldSendToHost = true;
break;
}
// Check to make sure we are sending to only observers, if not log an error.
if (NetworkManager.LogLevel >= LogLevel.Error && !NetworkObject.Observers.Contains(targetClientId))
{
NetworkLog.LogError(GenerateObserverErrorMessage(clientRpcParams, targetClientId));
}
}
rpcWriteSize = NetworkManager.SendMessage(ref clientRpcMessage, networkDelivery, clientRpcParams.Send.TargetClientIdsNativeArray.Value);
}
else
{
shouldSendToHost = IsHost;
rpcWriteSize = NetworkManager.SendMessage(ref clientRpcMessage, networkDelivery, NetworkManager.ConnectedClientsIds);
var observerEnumerator = NetworkObject.Observers.GetEnumerator();
while (observerEnumerator.MoveNext())
{
// Skip over the host
if (IsHost && observerEnumerator.Current == NetworkManager.LocalClientId)
{
shouldSendToHost = true;
continue;
}
rpcWriteSize = NetworkManager.MessagingSystem.SendMessage(ref clientRpcMessage, networkDelivery, observerEnumerator.Current);
}
}
// If we are a server/host then we just no op and send to ourself
@@ -228,6 +248,12 @@ namespace Unity.Netcode
#endif
}
internal string GenerateObserverErrorMessage(ClientRpcParams clientRpcParams, ulong targetClientId)
{
var containerNameHoldingId = clientRpcParams.Send.TargetClientIds != null ? nameof(ClientRpcParams.Send.TargetClientIds) : nameof(ClientRpcParams.Send.TargetClientIdsNativeArray);
return $"Sending ClientRpc to non-observer! {containerNameHoldingId} contains clientId {targetClientId} that is not an observer!";
}
/// <summary>
/// Gets the NetworkManager that owns this NetworkBehaviour instance
/// See note around `NetworkObject` for how there is a chicken / egg problem when we are not initialized
@@ -235,42 +261,42 @@ namespace Unity.Netcode
public NetworkManager NetworkManager => NetworkObject.NetworkManager;
/// <summary>
/// Gets if the object is the the personal clients player object
/// If a NetworkObject is assigned, it will return whether or not this NetworkObject
/// is the local player object. If no NetworkObject is assigned it will always return false.
/// </summary>
public bool IsLocalPlayer => NetworkObject.IsLocalPlayer;
public bool IsLocalPlayer { get; private set; }
/// <summary>
/// Gets if the object is owned by the local player or if the object is the local player object
/// </summary>
public bool IsOwner => NetworkObject.IsOwner;
public bool IsOwner { get; internal set; }
/// <summary>
/// Gets if we are executing as server
/// </summary>
protected bool IsServer => IsRunning && NetworkManager.IsServer;
protected bool IsServer { get; private set; }
/// <summary>
/// Gets if we are executing as client
/// </summary>
protected bool IsClient => IsRunning && NetworkManager.IsClient;
protected bool IsClient { get; private set; }
/// <summary>
/// Gets if we are executing as Host, I.E Server and Client
/// </summary>
protected bool IsHost => IsRunning && NetworkManager.IsHost;
private bool IsRunning => NetworkManager && NetworkManager.IsListening;
protected bool IsHost { get; private set; }
/// <summary>
/// Gets Whether or not the object has a owner
/// </summary>
public bool IsOwnedByServer => NetworkObject.IsOwnedByServer;
public bool IsOwnedByServer { get; internal set; }
/// <summary>
/// Used to determine if it is safe to access NetworkObject and NetworkManager from within a NetworkBehaviour component
/// Primarily useful when checking NetworkObject/NetworkManager properties within FixedUpate
/// </summary>
public bool IsSpawned => HasNetworkObject ? NetworkObject.IsSpawned : false;
public bool IsSpawned { get; internal set; }
internal bool IsBehaviourEditable()
{
@@ -327,12 +353,12 @@ namespace Unity.Netcode
/// <summary>
/// Gets the NetworkId of the NetworkObject that owns this NetworkBehaviour
/// </summary>
public ulong NetworkObjectId => NetworkObject.NetworkObjectId;
public ulong NetworkObjectId { get; internal set; }
/// <summary>
/// Gets NetworkId for this NetworkBehaviour from the owner NetworkObject
/// </summary>
public ushort NetworkBehaviourId => NetworkObject.GetNetworkBehaviourOrderIndex(this);
public ushort NetworkBehaviourId { get; internal set; }
/// <summary>
/// Internally caches the Id of this behaviour in a NetworkObject. Makes look-up faster
@@ -352,7 +378,47 @@ namespace Unity.Netcode
/// <summary>
/// Gets the ClientId that owns the NetworkObject
/// </summary>
public ulong OwnerClientId => NetworkObject.OwnerClientId;
public ulong OwnerClientId { get; internal set; }
/// <summary>
/// Updates properties with network session related
/// dependencies such as a NetworkObject's spawned
/// state or NetworkManager's session state.
/// </summary>
internal void UpdateNetworkProperties()
{
// Set NetworkObject dependent properties
if (NetworkObject != null)
{
// Set identification related properties
NetworkObjectId = NetworkObject.NetworkObjectId;
IsLocalPlayer = NetworkObject.IsLocalPlayer;
// This is "OK" because GetNetworkBehaviourOrderIndex uses the order of
// NetworkObject.ChildNetworkBehaviours which is set once when first
// accessed.
NetworkBehaviourId = NetworkObject.GetNetworkBehaviourOrderIndex(this);
// Set ownership related properties
IsOwnedByServer = NetworkObject.IsOwnedByServer;
IsOwner = NetworkObject.IsOwner;
OwnerClientId = NetworkObject.OwnerClientId;
// Set NetworkManager dependent properties
if (NetworkManager != null)
{
IsHost = NetworkManager.IsListening && NetworkManager.IsHost;
IsClient = NetworkManager.IsListening && NetworkManager.IsClient;
IsServer = NetworkManager.IsListening && NetworkManager.IsServer;
}
}
else // Shouldn't happen, but if so then set the properties to their default value;
{
OwnerClientId = NetworkObjectId = default;
IsOwnedByServer = IsOwner = IsHost = IsClient = IsServer = default;
NetworkBehaviourId = default;
}
}
/// <summary>
/// Gets called when the <see cref="NetworkObject"/> gets spawned, message handlers are ready to be registered and the network is setup.
@@ -366,21 +432,41 @@ namespace Unity.Netcode
internal void InternalOnNetworkSpawn()
{
IsSpawned = true;
InitializeVariables();
UpdateNetworkProperties();
OnNetworkSpawn();
}
internal void InternalOnNetworkDespawn() { }
internal void InternalOnNetworkDespawn()
{
IsSpawned = false;
UpdateNetworkProperties();
OnNetworkDespawn();
}
/// <summary>
/// Gets called when the local client gains ownership of this object
/// </summary>
public virtual void OnGainedOwnership() { }
internal void InternalOnGainedOwnership()
{
UpdateNetworkProperties();
OnGainedOwnership();
}
/// <summary>
/// Gets called when we loose ownership of this object
/// </summary>
public virtual void OnLostOwnership() { }
internal void InternalOnLostOwnership()
{
UpdateNetworkProperties();
OnLostOwnership();
}
/// <summary>
/// Gets called when the parent NetworkObject of this NetworkBehaviour's NetworkObject has changed
/// </summary>
@@ -433,12 +519,10 @@ namespace Unity.Netcode
m_VarInit = true;
FieldInfo[] sortedFields = GetFieldInfoForType(GetType());
var sortedFields = GetFieldInfoForType(GetType());
for (int i = 0; i < sortedFields.Length; i++)
{
Type fieldType = sortedFields[i].FieldType;
var fieldType = sortedFields[i].FieldType;
if (fieldType.IsSubclassOf(typeof(NetworkVariableBase)))
{
var instance = (NetworkVariableBase)sortedFields[i].GetValue(this);
@@ -499,7 +583,7 @@ namespace Unity.Netcode
}
}
internal void VariableUpdate(ulong clientId)
internal void VariableUpdate(ulong targetClientId)
{
if (!m_VarInit)
{
@@ -507,67 +591,58 @@ namespace Unity.Netcode
}
PreNetworkVariableWrite();
NetworkVariableUpdate(clientId, NetworkBehaviourId);
NetworkVariableUpdate(targetClientId, NetworkBehaviourId);
}
internal readonly List<int> NetworkVariableIndexesToReset = new List<int>();
internal readonly HashSet<int> NetworkVariableIndexesToResetSet = new HashSet<int>();
private void NetworkVariableUpdate(ulong clientId, int behaviourIndex)
private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex)
{
if (!CouldHaveDirtyNetworkVariables())
{
return;
}
if (NetworkManager.NetworkConfig.UseSnapshotDelta)
for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++)
{
var shouldSend = false;
for (int k = 0; k < NetworkVariableFields.Count; k++)
{
NetworkManager.SnapshotSystem.Store(NetworkObjectId, behaviourIndex, k, NetworkVariableFields[k]);
}
}
if (!NetworkManager.NetworkConfig.UseSnapshotDelta)
{
for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++)
{
var shouldSend = false;
for (int k = 0; k < NetworkVariableFields.Count; k++)
var networkVariable = NetworkVariableFields[k];
if (networkVariable.IsDirty() && networkVariable.CanClientRead(targetClientId))
{
if (NetworkVariableFields[k].ShouldWrite(clientId, IsServer))
shouldSend = true;
break;
}
}
if (shouldSend)
{
var message = new NetworkVariableDeltaMessage
{
NetworkObjectId = NetworkObjectId,
NetworkBehaviourIndex = NetworkObject.GetNetworkBehaviourOrderIndex(this),
NetworkBehaviour = this,
TargetClientId = targetClientId,
DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j]
};
// TODO: Serialization is where the IsDirty flag gets changed.
// Messages don't get sent from the server to itself, so if we're host and sending to ourselves,
// we still have to actually serialize the message even though we're not sending it, otherwise
// the dirty flag doesn't change properly. These two pieces should be decoupled at some point
// so we don't have to do this serialization work if we're not going to use the result.
if (IsServer && targetClientId == NetworkManager.ServerClientId)
{
var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE);
using (tmpWriter)
{
shouldSend = true;
message.Serialize(tmpWriter);
}
}
if (shouldSend)
else
{
var message = new NetworkVariableDeltaMessage
{
NetworkObjectId = NetworkObjectId,
NetworkBehaviourIndex = NetworkObject.GetNetworkBehaviourOrderIndex(this),
NetworkBehaviour = this,
ClientId = clientId,
DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j]
};
// TODO: Serialization is where the IsDirty flag gets changed.
// Messages don't get sent from the server to itself, so if we're host and sending to ourselves,
// we still have to actually serialize the message even though we're not sending it, otherwise
// the dirty flag doesn't change properly. These two pieces should be decoupled at some point
// so we don't have to do this serialization work if we're not going to use the result.
if (IsServer && clientId == NetworkManager.ServerClientId)
{
var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE);
using (tmpWriter)
{
message.Serialize(tmpWriter);
}
}
else
{
NetworkManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], clientId);
}
NetworkManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], targetClientId);
}
}
}
@@ -595,7 +670,7 @@ namespace Unity.Netcode
}
}
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId)
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId)
{
if (NetworkVariableFields.Count == 0)
{
@@ -604,7 +679,7 @@ namespace Unity.Netcode
for (int j = 0; j < NetworkVariableFields.Count; j++)
{
bool canClientRead = NetworkVariableFields[j].CanClientRead(clientId);
bool canClientRead = NetworkVariableFields[j].CanClientRead(targetClientId);
if (canClientRead)
{

View File

@@ -57,7 +57,7 @@ namespace Unity.Netcode
{
for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++)
{
sobj.ChildNetworkBehaviours[k].VariableUpdate(networkManager.ServerClientId);
sobj.ChildNetworkBehaviours[k].VariableUpdate(NetworkManager.ServerClientId);
}
}
}

View File

@@ -12,6 +12,7 @@ using Unity.Multiplayer.Tools;
#endif
using Unity.Profiling;
using UnityEngine.SceneManagement;
using System.Runtime.CompilerServices;
using Debug = UnityEngine.Debug;
namespace Unity.Netcode
@@ -45,7 +46,7 @@ namespace Unity.Netcode
private static ProfilerMarker s_TransportDisconnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportDisconnect");
#endif
private const double k_TimeSyncFrequency = 1.0d; // sync every second, TODO will be removed once timesync is done via snapshots
private const double k_TimeSyncFrequency = 1.0d; // sync every second
private const float k_DefaultBufferSizeSec = 0.05f; // todo talk with UX/Product, find good default value for this
internal static string PrefabDebugHelper(NetworkPrefab networkPrefab)
@@ -53,7 +54,6 @@ namespace Unity.Netcode
return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.gameObject.name}\"";
}
internal SnapshotSystem SnapshotSystem { get; private set; }
internal NetworkBehaviourUpdater BehaviourUpdater { get; private set; }
internal MessagingSystem MessagingSystem { get; private set; }
@@ -125,13 +125,11 @@ namespace Unity.Netcode
public bool OnVerifyCanReceive(ulong senderId, Type messageType)
{
if (m_NetworkManager.PendingClients.TryGetValue(senderId, out PendingClient client) &&
(client.ConnectionState == PendingClient.State.PendingApproval ||
(client.ConnectionState == PendingClient.State.PendingConnection &&
messageType != typeof(ConnectionRequestMessage))))
(client.ConnectionState == PendingClient.State.PendingApproval || (client.ConnectionState == PendingClient.State.PendingConnection && messageType != typeof(ConnectionRequestMessage))))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId.ToString()} before it has been accepted");
NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId} before it has been accepted");
}
return false;
@@ -229,14 +227,12 @@ namespace Unity.Netcode
public NetworkSceneManager SceneManager { get; private set; }
public readonly ulong ServerClientId = 0;
public const ulong ServerClientId = 0;
/// <summary>
/// Gets the networkId of the server
/// </summary>
private ulong m_ServerTransportId => NetworkConfig.NetworkTransport?.ServerClientId ??
throw new NullReferenceException(
$"The transport in the active {nameof(NetworkConfig)} is null");
private ulong m_ServerTransportId => NetworkConfig.NetworkTransport?.ServerClientId ?? throw new NullReferenceException($"The transport in the active {nameof(NetworkConfig)} is null");
/// <summary>
/// Returns ServerClientId if IsServer or LocalClientId if not
@@ -367,16 +363,14 @@ namespace Unity.Netcode
/// <param name="approved">Whether or not the client was approved</param>
/// <param name="position">The position to spawn the client at. If null, the prefab position is used.</param>
/// <param name="rotation">The rotation to spawn the client with. If null, the prefab position is used.</param>
public delegate void ConnectionApprovedDelegate(bool createPlayerObject, uint? playerPrefabHash, bool approved,
Vector3? position, Quaternion? rotation);
public delegate void ConnectionApprovedDelegate(bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation);
/// <summary>
/// The callback to invoke during connection approval
/// </summary>
public event Action<byte[], ulong, ConnectionApprovedDelegate> ConnectionApprovalCallback = null;
internal void InvokeConnectionApproval(byte[] payload, ulong clientId, ConnectionApprovedDelegate action) =>
ConnectionApprovalCallback?.Invoke(payload, clientId, action);
internal void InvokeConnectionApproval(byte[] payload, ulong clientId, ConnectionApprovedDelegate action) => ConnectionApprovalCallback?.Invoke(payload, clientId, action);
/// <summary>
/// The current NetworkConfig
@@ -565,13 +559,6 @@ namespace Unity.Netcode
NetworkConfig.NetworkTransport.NetworkMetrics = NetworkMetrics;
//This 'if' should never enter
if (SnapshotSystem != null)
{
SnapshotSystem.Dispose();
SnapshotSystem = null;
}
if (server)
{
NetworkTimeSystem = NetworkTimeSystem.ServerTimeSystem();
@@ -584,8 +571,6 @@ namespace Unity.Netcode
NetworkTickSystem = new NetworkTickSystem(NetworkConfig.TickRate, 0, 0);
NetworkTickSystem.Tick += OnNetworkManagerTick;
SnapshotSystem = new SnapshotSystem(this, NetworkConfig, NetworkTickSystem);
this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate);
// This is used to remove entries not needed or invalid
@@ -800,44 +785,35 @@ namespace Unity.Netcode
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo("StartServer()");
NetworkLog.LogInfo(nameof(StartServer));
}
if (IsServer || IsClient)
if (!CanStart(StartType.Server))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Cannot start server while an instance is already running");
}
return false;
}
if (NetworkConfig.ConnectionApproval)
{
if (ConnectionApprovalCallback == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"No ConnectionApproval callback defined. Connection approval will timeout");
}
}
}
Initialize(true);
var result = NetworkConfig.NetworkTransport.StartServer();
// If we failed to start then shutdown and notify user that the transport failed to start
if (NetworkConfig.NetworkTransport.StartServer())
{
IsServer = true;
IsClient = false;
IsListening = true;
IsServer = true;
IsClient = false;
IsListening = true;
SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
OnServerStarted?.Invoke();
return true;
}
else
{
Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!");
Shutdown();
}
OnServerStarted?.Invoke();
return result;
return false;
}
/// <summary>
@@ -850,26 +826,26 @@ namespace Unity.Netcode
NetworkLog.LogInfo(nameof(StartClient));
}
if (IsServer || IsClient)
if (!CanStart(StartType.Client))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Cannot start client while an instance is already running");
}
return false;
}
Initialize(false);
MessagingSystem.ClientConnected(ServerClientId);
var result = NetworkConfig.NetworkTransport.StartClient();
if (!NetworkConfig.NetworkTransport.StartClient())
{
Debug.LogError($"Client is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!");
Shutdown();
return false;
}
IsServer = false;
IsClient = true;
IsListening = true;
return result;
return true;
}
/// <summary>
@@ -882,31 +858,21 @@ namespace Unity.Netcode
NetworkLog.LogInfo(nameof(StartHost));
}
if (IsServer || IsClient)
if (!CanStart(StartType.Host))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Cannot start host while an instance is already running");
}
return false;
}
if (NetworkConfig.ConnectionApproval)
{
if (ConnectionApprovalCallback == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"No ConnectionApproval callback defined. Connection approval will timeout");
}
}
}
Initialize(true);
var result = NetworkConfig.NetworkTransport.StartServer();
// If we failed to start then shutdown and notify user that the transport failed to start
if (!NetworkConfig.NetworkTransport.StartServer())
{
Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!");
Shutdown();
return false;
}
MessagingSystem.ClientConnected(ServerClientId);
LocalClientId = ServerClientId;
NetworkMetrics.SetConnectionId(LocalClientId);
@@ -942,7 +908,53 @@ namespace Unity.Netcode
OnServerStarted?.Invoke();
return result;
return true;
}
private enum StartType
{
Server,
Host,
Client
}
private bool CanStart(StartType type)
{
if (IsListening)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Cannot start " + type + " while an instance is already running");
}
return false;
}
if (NetworkConfig.ConnectionApproval)
{
if (ConnectionApprovalCallback == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"No ConnectionApproval callback defined. Connection approval will timeout");
}
}
}
if (ConnectionApprovalCallback != null)
{
if (!NetworkConfig.ConnectionApproval)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"A ConnectionApproval callback is defined but ConnectionApproval is disabled. In order to use ConnectionApproval it has to be explicitly enabled ");
}
}
}
return true;
}
public void SetSingleton()
@@ -1014,6 +1026,7 @@ namespace Unity.Netcode
internal interface INetworkManagerHelper
{
bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool ignoreNetworkManagerCache = false, bool editorTest = false);
void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false);
}
#endif
@@ -1133,12 +1146,6 @@ namespace Unity.Netcode
this.UnregisterAllNetworkUpdates();
if (SnapshotSystem != null)
{
SnapshotSystem.Dispose();
SnapshotSystem = null;
}
if (NetworkTickSystem != null)
{
NetworkTickSystem.Tick -= OnNetworkManagerTick;
@@ -1274,7 +1281,11 @@ namespace Unity.Netcode
if (!m_ShuttingDown || !m_StopProcessingMessages)
{
MessagingSystem.ProcessSendQueues();
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
NetworkMetrics.DispatchFrame();
NetworkObject.VerifyParentingStatus();
}
SpawnManager.CleanupStaleTriggers();
@@ -1288,7 +1299,6 @@ namespace Unity.Netcode
/// This function runs once whenever the local tick is incremented and is responsible for the following (in order):
/// - collect commands/inputs and send them to the server (TBD)
/// - call NetworkFixedUpdate on all NetworkBehaviours in prediction/client authority mode
/// - create a snapshot from resulting state
/// </summary>
private void OnNetworkManagerTick()
{
@@ -1419,18 +1429,15 @@ namespace Unity.Netcode
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportDisconnect.Begin();
#endif
clientId = TransportIdToClientId(clientId);
OnClientDisconnectCallback?.Invoke(clientId);
m_TransportIdToClientIdMap.Remove(transportId);
m_ClientIdToTransportIdMap.Remove(clientId);
clientId = TransportIdCleanUp(clientId, transportId);
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo($"Disconnect Event From {clientId}");
}
OnClientDisconnectCallback?.Invoke(clientId);
if (IsServer)
{
OnClientDisconnectFromServer(clientId);
@@ -1446,6 +1453,31 @@ namespace Unity.Netcode
}
}
/// <summary>
/// Handles cleaning up the transport id/client id tables after
/// receiving a disconnect event from transport
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ulong TransportIdCleanUp(ulong clientId, ulong transportId)
{
// This check is for clients that attempted to connect but failed.
// When this happens, the client will not have an entry within the
// m_TransportIdToClientIdMap or m_ClientIdToTransportIdMap lookup
// tables so we exit early and just return 0 to be used for the
// disconnect event.
if (!IsServer && !m_TransportIdToClientIdMap.ContainsKey(clientId))
{
return 0;
}
clientId = TransportIdToClientId(clientId);
m_TransportIdToClientIdMap.Remove(transportId);
m_ClientIdToTransportIdMap.Remove(clientId);
return clientId;
}
internal unsafe int SendMessage<TMessageType, TClientIdListType>(ref TMessageType message, NetworkDelivery delivery, in TClientIdListType clientIds)
where TMessageType : INetworkMessage
where TClientIdListType : IReadOnlyList<ulong>
@@ -1591,32 +1623,45 @@ namespace Unity.Netcode
}
}
for (int i = networkClient.OwnedObjects.Count - 1; i >= 0; i--)
// Get the NetworkObjects owned by the disconnected client
var clientOwnedObjects = SpawnManager.GetClientOwnedObjects(clientId);
if (clientOwnedObjects == null)
{
var ownedObject = networkClient.OwnedObjects[i];
if (ownedObject != null)
// This could happen if a client is never assigned a player object and is disconnected
// Only log this in verbose/developer mode
if (LogLevel == LogLevel.Developer)
{
if (!ownedObject.DontDestroyWithOwner)
NetworkLog.LogWarning($"ClientID {clientId} disconnected with (0) zero owned objects! Was a player prefab not assigned?");
}
}
else
{
// Handle changing ownership and prefab handlers
for (int i = clientOwnedObjects.Count - 1; i >= 0; i--)
{
var ownedObject = clientOwnedObjects[i];
if (ownedObject != null)
{
if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].OwnedObjects[i]
.GlobalObjectIdHash))
if (!ownedObject.DontDestroyWithOwner)
{
PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].OwnedObjects[i]);
if (PrefabHandler.ContainsHandler(clientOwnedObjects[i].GlobalObjectIdHash))
{
PrefabHandler.HandleNetworkPrefabDestroy(clientOwnedObjects[i]);
}
else
{
Destroy(ownedObject.gameObject);
}
}
else
{
Destroy(ownedObject.gameObject);
ownedObject.RemoveOwnership();
}
}
else
{
ownedObject.RemoveOwnership();
}
}
}
// TODO: Could(should?) be replaced with more memory per client, by storing the visibility
foreach (var sobj in SpawnManager.SpawnedObjectsList)
{
sobj.Observers.Remove(clientId);
@@ -1764,7 +1809,7 @@ namespace Unity.Netcode
var message = new CreateObjectMessage
{
ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key, false)
ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key)
};
message.ObjectInfo.Header.Hash = playerPrefabHash;
message.ObjectInfo.Header.IsSceneObject = false;

View File

@@ -54,8 +54,6 @@ namespace Unity.Netcode
/// </summary>
internal NetworkManager NetworkManagerOwner;
private ulong m_NetworkObjectId;
/// <summary>
/// Gets the unique Id of this object that is synced across the network
/// </summary>
@@ -64,33 +62,7 @@ namespace Unity.Netcode
/// <summary>
/// Gets the ClientId of the owner of this NetworkObject
/// </summary>
public ulong OwnerClientId
{
get
{
if (OwnerClientIdInternal == null)
{
return NetworkManager != null ? NetworkManager.ServerClientId : 0;
}
else
{
return OwnerClientIdInternal.Value;
}
}
internal set
{
if (NetworkManager != null && value == NetworkManager.ServerClientId)
{
OwnerClientIdInternal = null;
}
else
{
OwnerClientIdInternal = value;
}
}
}
internal ulong? OwnerClientIdInternal = null;
public ulong OwnerClientId { get; internal set; }
/// <summary>
/// If true, the object will always be replicated as root on clients and the parent will be ignored.
@@ -234,11 +206,6 @@ namespace Unity.Netcode
throw new VisibilityChangeException("The object is already visible");
}
if (NetworkManager.NetworkConfig.UseSnapshotSpawn)
{
SnapshotSpawn(clientId);
}
Observers.Add(clientId);
NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, this);
@@ -314,23 +281,15 @@ namespace Unity.Netcode
throw new VisibilityChangeException("Cannot hide an object from the server");
}
Observers.Remove(clientId);
if (NetworkManager.NetworkConfig.UseSnapshotSpawn)
var message = new DestroyObjectMessage
{
SnapshotDespawn(clientId);
}
else
{
var message = new DestroyObjectMessage
{
NetworkObjectId = NetworkObjectId
};
// Send destroy call
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId);
NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size);
}
NetworkObjectId = NetworkObjectId
};
// Send destroy call
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId);
NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size);
}
/// <summary>
@@ -345,14 +304,14 @@ namespace Unity.Netcode
throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided");
}
NetworkManager networkManager = networkObjects[0].NetworkManager;
var networkManager = networkObjects[0].NetworkManager;
if (!networkManager.IsServer)
{
throw new NotServerException("Only server can change visibility");
}
if (clientId == networkManager.ServerClientId)
if (clientId == NetworkManager.ServerClientId)
{
throw new VisibilityChangeException("Cannot hide an object from the server");
}
@@ -384,84 +343,21 @@ namespace Unity.Netcode
private void OnDestroy()
{
if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned
&& (IsSceneObject == null || (IsSceneObject != null && IsSceneObject.Value != true)))
if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned &&
(IsSceneObject == null || (IsSceneObject != null && IsSceneObject.Value != true)))
{
throw new NotServerException($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead.");
}
if (NetworkManager != null && NetworkManager.SpawnManager != null && NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject))
if (NetworkManager != null && NetworkManager.SpawnManager != null &&
NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject))
{
NetworkManager.SpawnManager.OnDespawnObject(networkObject, false);
}
}
private SnapshotDespawnCommand GetDespawnCommand()
{
var command = new SnapshotDespawnCommand();
command.NetworkObjectId = NetworkObjectId;
return command;
}
private SnapshotSpawnCommand GetSpawnCommand()
{
var command = new SnapshotSpawnCommand();
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;
return command;
}
private void SnapshotSpawn()
{
var command = GetSpawnCommand();
NetworkManager.SnapshotSystem.Spawn(command);
}
private void SnapshotSpawn(ulong clientId)
{
var command = GetSpawnCommand();
command.TargetClientIds = new List<ulong>();
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<ulong>();
command.TargetClientIds.Add(clientId);
NetworkManager.SnapshotSystem.Despawn(command);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SpawnInternal(bool destroyWithScene, ulong? ownerClientId, bool playerObject)
private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject)
{
if (!NetworkManager.IsListening)
{
@@ -475,12 +371,6 @@ namespace Unity.Netcode
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), false, playerObject, ownerClientId, destroyWithScene);
if (NetworkManager.NetworkConfig.UseSnapshotSpawn)
{
SnapshotSpawn();
}
ulong ownerId = ownerClientId != null ? ownerClientId.Value : NetworkManager.ServerClientId;
for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++)
{
if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId))
@@ -496,7 +386,7 @@ namespace Unity.Netcode
/// <param name="destroyWithScene">Should the object be destroyed when the scene is changed</param>
public void Spawn(bool destroyWithScene = false)
{
SpawnInternal(destroyWithScene, null, false);
SpawnInternal(destroyWithScene, NetworkManager.ServerClientId, false);
}
/// <summary>
@@ -547,17 +437,29 @@ namespace Unity.Netcode
internal void InvokeBehaviourOnLostOwnership()
{
// Server already handles this earlier, hosts should ignore, all clients should update
if (!NetworkManager.IsServer)
{
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true);
}
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
ChildNetworkBehaviours[i].OnLostOwnership();
ChildNetworkBehaviours[i].InternalOnLostOwnership();
}
}
internal void InvokeBehaviourOnGainedOwnership()
{
// Server already handles this earlier, hosts should ignore and only client owners should update
if (!NetworkManager.IsServer && NetworkManager.LocalClientId == OwnerClientId)
{
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId);
}
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
ChildNetworkBehaviours[i].OnGainedOwnership();
ChildNetworkBehaviours[i].InternalOnGainedOwnership();
}
}
@@ -756,13 +658,7 @@ namespace Unity.Netcode
if (!NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value))
{
if (OrphanChildren.Add(this))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{nameof(NetworkObject)} ({name}) cannot find its parent, added to {nameof(OrphanChildren)} set");
}
}
OrphanChildren.Add(this);
return false;
}
@@ -793,19 +689,21 @@ namespace Unity.Netcode
internal void InvokeBehaviourNetworkSpawn()
{
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId);
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
ChildNetworkBehaviours[i].InternalOnNetworkSpawn();
ChildNetworkBehaviours[i].OnNetworkSpawn();
}
}
internal void InvokeBehaviourNetworkDespawn()
{
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true);
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
ChildNetworkBehaviours[i].InternalOnNetworkDespawn();
ChildNetworkBehaviours[i].OnNetworkDespawn();
}
}
@@ -834,13 +732,13 @@ namespace Unity.Netcode
}
}
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId)
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId)
{
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
{
var behavior = ChildNetworkBehaviours[i];
behavior.InitializeVariables();
behavior.WriteNetworkVariableData(writer, clientId);
behavior.WriteNetworkVariableData(writer, targetClientId);
}
}
@@ -853,6 +751,27 @@ namespace Unity.Netcode
}
}
// NGO currently guarantees that the client will receive spawn data for all objects in one network tick.
// Children may arrive before their parents; when they do they are stored in OrphanedChildren and then
// resolved when their parents arrived. Because we don't send a partial list of spawns (yet), something
// has gone wrong if by the end of an update we still have unresolved orphans
//
// if and when we have different systems for where it is expected that orphans survive across ticks,
// then this warning will remind us that we need to revamp the system because then we can no longer simply
// spawn the orphan without its parent (at least, not when its transform is set to local coords mode)
// - because then youll have children popping at the wrong location not having their parents global position to root them
// - and then theyll pop to the correct location after they get the parent, and that would be not good
internal static void VerifyParentingStatus()
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
if (OrphanChildren.Count > 0)
{
NetworkLog.LogWarning($"{nameof(NetworkObject)} ({OrphanChildren.Count}) children not resolved to parents by the end of frame");
}
}
}
internal void SetNetworkVariableData(FastBufferReader reader)
{
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
@@ -918,7 +837,6 @@ namespace Unity.Netcode
public bool IsSceneObject;
public bool HasTransform;
public bool IsReparented;
public bool HasNetworkVariables;
}
public HeaderData Header;
@@ -979,10 +897,7 @@ namespace Unity.Netcode
}
}
if (Header.HasNetworkVariables)
{
OwnerObject.WriteNetworkVariableData(writer, TargetClientId);
}
OwnerObject.WriteNetworkVariableData(writer, TargetClientId);
}
public unsafe void Deserialize(FastBufferReader reader)
@@ -1022,7 +937,7 @@ namespace Unity.Netcode
}
}
internal SceneObject GetMessageSceneObject(ulong targetClientId, bool includeNetworkVariableData = true)
internal SceneObject GetMessageSceneObject(ulong targetClientId)
{
var obj = new SceneObject
{
@@ -1033,7 +948,6 @@ namespace Unity.Netcode
OwnerClientId = OwnerClientId,
IsSceneObject = IsSceneObject ?? true,
Hash = HostCheckForGlobalObjectIdHashOverride(),
HasNetworkVariables = includeNetworkVariableData
},
OwnerObject = this,
TargetClientId = targetClientId

View File

@@ -1,93 +0,0 @@
using System;
namespace Unity.Netcode
{
internal class ConnectionRtt
{
private double[] m_RttSendTimes; // times at which packet were sent for RTT computations
private int[] m_SendSequence; // tick, or other key, at which packets were sent (to allow matching)
private double[] m_MeasuredLatencies; // measured latencies (ring buffer)
private int m_LatenciesBegin = 0; // ring buffer begin
private int m_LatenciesEnd = 0; // ring buffer end
/// <summary>
/// Round-trip-time data
/// </summary>
public struct Rtt
{
public double BestSec; // best RTT
public double AverageSec; // average RTT
public double WorstSec; // worst RTT
public double LastSec; // latest ack'ed RTT
public int SampleCount; // number of contributing samples
}
public ConnectionRtt()
{
m_RttSendTimes = new double[NetworkConfig.RttWindowSize];
m_SendSequence = new int[NetworkConfig.RttWindowSize];
m_MeasuredLatencies = new double[NetworkConfig.RttWindowSize];
}
/// <summary>
/// Returns the Round-trip-time computation for this client
/// </summary>
public Rtt GetRtt()
{
var ret = new Rtt();
var index = m_LatenciesBegin;
double total = 0.0;
ret.BestSec = m_MeasuredLatencies[m_LatenciesBegin];
ret.WorstSec = m_MeasuredLatencies[m_LatenciesBegin];
while (index != m_LatenciesEnd)
{
total += m_MeasuredLatencies[index];
ret.SampleCount++;
ret.BestSec = Math.Min(ret.BestSec, m_MeasuredLatencies[index]);
ret.WorstSec = Math.Max(ret.WorstSec, m_MeasuredLatencies[index]);
index = (index + 1) % NetworkConfig.RttAverageSamples;
}
if (ret.SampleCount != 0)
{
ret.AverageSec = total / ret.SampleCount;
// the latest RTT is one before m_LatenciesEnd
ret.LastSec = m_MeasuredLatencies[(m_LatenciesEnd + (NetworkConfig.RttWindowSize - 1)) % NetworkConfig.RttWindowSize];
}
else
{
ret.AverageSec = 0;
ret.BestSec = 0;
ret.WorstSec = 0;
ret.SampleCount = 0;
ret.LastSec = 0;
}
return ret;
}
internal void NotifySend(int sequence, double timeSec)
{
m_RttSendTimes[sequence % NetworkConfig.RttWindowSize] = timeSec;
m_SendSequence[sequence % NetworkConfig.RttWindowSize] = sequence;
}
internal void NotifyAck(int sequence, double timeSec)
{
// if the same slot was not used by a later send
if (m_SendSequence[sequence % NetworkConfig.RttWindowSize] == sequence)
{
double latency = timeSec - m_RttSendTimes[sequence % NetworkConfig.RttWindowSize];
m_MeasuredLatencies[m_LatenciesEnd] = latency;
m_LatenciesEnd = (m_LatenciesEnd + 1) % NetworkConfig.RttAverageSamples;
if (m_LatenciesEnd == m_LatenciesBegin)
{
m_LatenciesBegin = (m_LatenciesBegin + 1) % NetworkConfig.RttAverageSamples;
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,9 @@ namespace Unity.Netcode
public static LogLevel CurrentLogLevel => NetworkManager.Singleton == null ? LogLevel.Normal : NetworkManager.Singleton.LogLevel;
// internal logging
internal static void LogInfo(string message) => Debug.Log($"[Netcode] {message}");
internal static void LogWarning(string message) => Debug.LogWarning($"[Netcode] {message}");
internal static void LogError(string message) => Debug.LogError($"[Netcode] {message}");
public static void LogInfo(string message) => Debug.Log($"[Netcode] {message}");
public static void LogWarning(string message) => Debug.LogWarning($"[Netcode] {message}");
public static void LogError(string message) => Debug.LogError($"[Netcode] {message}");
/// <summary>
/// Logs an info log locally and on the server if possible.
@@ -62,9 +62,9 @@ namespace Unity.Netcode
LogType = logType,
Message = message
};
var size = NetworkManager.Singleton.SendMessage(ref networkMessage, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.Singleton.ServerClientId);
var size = NetworkManager.Singleton.SendMessage(ref networkMessage, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId);
NetworkManager.Singleton.NetworkMetrics.TrackServerLogSent(NetworkManager.Singleton.ServerClientId, (uint)logType, size);
NetworkManager.Singleton.NetworkMetrics.TrackServerLogSent(NetworkManager.ServerClientId, (uint)logType, size);
}
}

View File

@@ -29,24 +29,33 @@ namespace Unity.Netcode
public void Handle(ref NetworkContext context)
{
var networkManager = (NetworkManager)context.SystemOwner;
var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId];
if (networkObject.OwnerClientId == networkManager.LocalClientId)
{
//We are current owner.
networkObject.InvokeBehaviourOnLostOwnership();
}
var originalOwner = networkObject.OwnerClientId;
networkObject.OwnerClientId = OwnerClientId;
// We are current owner.
if (originalOwner == networkManager.LocalClientId)
{
networkObject.InvokeBehaviourOnLostOwnership();
}
// We are new owner.
if (OwnerClientId == networkManager.LocalClientId)
{
//We are new owner.
networkObject.InvokeBehaviourOnGainedOwnership();
}
// For all other clients that are neither the former or current owner, update the behaviours' properties
if (OwnerClientId != networkManager.LocalClientId && originalOwner != networkManager.LocalClientId)
{
for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++)
{
networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties();
}
}
networkManager.NetworkMetrics.TrackOwnershipChangeReceived(context.SenderId, networkObject, context.MessageSize);
}
}

View File

@@ -16,7 +16,7 @@ namespace Unity.Netcode
public ushort NetworkBehaviourIndex;
public HashSet<int> DeliveryMappedNetworkVariableIndex;
public ulong ClientId;
public ulong TargetClientId;
public NetworkBehaviour NetworkBehaviour;
private FastBufferReader m_ReceivedNetworkVariableData;
@@ -31,9 +31,9 @@ namespace Unity.Netcode
writer.WriteValue(NetworkObjectId);
writer.WriteValue(NetworkBehaviourIndex);
for (int k = 0; k < NetworkBehaviour.NetworkVariableFields.Count; k++)
for (int i = 0; i < NetworkBehaviour.NetworkVariableFields.Count; i++)
{
if (!DeliveryMappedNetworkVariableIndex.Contains(k))
if (!DeliveryMappedNetworkVariableIndex.Contains(i))
{
// This var does not belong to the currently iterating delivery group.
if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
@@ -48,15 +48,17 @@ namespace Unity.Netcode
continue;
}
// if I'm dirty AND a client, write (server always has all permissions)
// if I'm dirty AND the server AND the client can read me, send.
bool shouldWrite = NetworkBehaviour.NetworkVariableFields[k].ShouldWrite(ClientId, NetworkBehaviour.NetworkManager.IsServer);
var startingSize = writer.Length;
var networkVariable = NetworkBehaviour.NetworkVariableFields[i];
var shouldWrite = networkVariable.IsDirty() &&
networkVariable.CanClientRead(TargetClientId) &&
(NetworkBehaviour.NetworkManager.IsServer || networkVariable.CanClientWrite(NetworkBehaviour.NetworkManager.LocalClientId));
if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
if (!shouldWrite)
{
writer.WriteValueSafe((ushort)0);
BytePacker.WriteValueBitPacked(writer, 0);
}
}
else
@@ -68,34 +70,34 @@ namespace Unity.Netcode
{
if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, short.MaxValue);
NetworkBehaviour.NetworkVariableFields[k].WriteDelta(tmpWriter);
var tempWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE);
NetworkBehaviour.NetworkVariableFields[i].WriteDelta(tempWriter);
BytePacker.WriteValueBitPacked(writer, tempWriter.Length);
if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize<ushort>() + tmpWriter.Length))
if (!writer.TryBeginWrite(tempWriter.Length))
{
throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}");
}
writer.WriteValue((ushort)tmpWriter.Length);
tmpWriter.CopyTo(writer);
tempWriter.CopyTo(writer);
}
else
{
NetworkBehaviour.NetworkVariableFields[k].WriteDelta(writer);
networkVariable.WriteDelta(writer);
}
if (!NetworkBehaviour.NetworkVariableIndexesToResetSet.Contains(k))
if (!NetworkBehaviour.NetworkVariableIndexesToResetSet.Contains(i))
{
NetworkBehaviour.NetworkVariableIndexesToResetSet.Add(k);
NetworkBehaviour.NetworkVariableIndexesToReset.Add(k);
NetworkBehaviour.NetworkVariableIndexesToResetSet.Add(i);
NetworkBehaviour.NetworkVariableIndexesToReset.Add(i);
}
NetworkBehaviour.NetworkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(
ClientId,
TargetClientId,
NetworkBehaviour.NetworkObject,
NetworkBehaviour.NetworkVariableFields[k].Name,
networkVariable.Name,
NetworkBehaviour.__getTypeName(),
writer.Length);
writer.Length - startingSize);
}
}
}
@@ -121,9 +123,9 @@ namespace Unity.Netcode
if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out NetworkObject networkObject))
{
NetworkBehaviour behaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex);
var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex);
if (behaviour == null)
if (networkBehaviour == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
@@ -132,13 +134,12 @@ namespace Unity.Netcode
}
else
{
for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++)
for (int i = 0; i < networkBehaviour.NetworkVariableFields.Count; i++)
{
ushort varSize = 0;
int varSize = 0;
if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
m_ReceivedNetworkVariableData.ReadValueSafe(out varSize);
ByteUnpacker.ReadValueBitPacked(m_ReceivedNetworkVariableData, out varSize);
if (varSize == 0)
{
@@ -154,15 +155,17 @@ namespace Unity.Netcode
}
}
if (networkManager.IsServer)
var networkVariable = networkBehaviour.NetworkVariableFields[i];
if (networkManager.IsServer && !networkVariable.CanClientWrite(context.SenderId))
{
// we are choosing not to fire an exception here, because otherwise a malicious client could use this to crash the server
if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogWarning($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}");
NetworkLog.LogError($"[{behaviour.NetworkVariableFields[i].GetType().Name}]");
NetworkLog.LogWarning($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}");
NetworkLog.LogError($"[{networkVariable.GetType().Name}]");
}
m_ReceivedNetworkVariableData.Seek(m_ReceivedNetworkVariableData.Position + varSize);
@@ -176,23 +179,23 @@ namespace Unity.Netcode
//A dummy read COULD be added to the interface for this situation, but it's just being too nice.
//This is after all a developer fault. A critical error should be fine.
// - TwoTen
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogError($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. No more variables can be read. This is critical. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}");
NetworkLog.LogError($"[{behaviour.NetworkVariableFields[i].GetType().Name}]");
NetworkLog.LogError($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. No more variables can be read. This is critical. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}");
NetworkLog.LogError($"[{networkVariable.GetType().Name}]");
}
return;
}
int readStartPos = m_ReceivedNetworkVariableData.Position;
behaviour.NetworkVariableFields[i].ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer);
networkVariable.ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer);
networkManager.NetworkMetrics.TrackNetworkVariableDeltaReceived(
context.SenderId,
networkObject,
behaviour.NetworkVariableFields[i].Name,
behaviour.__getTypeName(),
networkVariable.Name,
networkBehaviour.__getTypeName(),
context.MessageSize);
@@ -202,7 +205,7 @@ namespace Unity.Netcode
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Var delta read too far. {m_ReceivedNetworkVariableData.Position - (readStartPos + varSize)} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}");
NetworkLog.LogWarning($"Var delta read too far. {m_ReceivedNetworkVariableData.Position - (readStartPos + varSize)} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}");
}
m_ReceivedNetworkVariableData.Seek(readStartPos + varSize);
@@ -211,7 +214,7 @@ namespace Unity.Netcode
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Var delta read too little. {(readStartPos + varSize) - m_ReceivedNetworkVariableData.Position} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}");
NetworkLog.LogWarning($"Var delta read too little. {readStartPos + varSize - m_ReceivedNetworkVariableData.Position} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}");
}
m_ReceivedNetworkVariableData.Seek(readStartPos + varSize);

View File

@@ -1,160 +0,0 @@
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
namespace Unity.Netcode
{
internal struct SnapshotDataMessage : INetworkMessage
{
public int CurrentTick;
public ushort Sequence;
public ushort Range;
public byte[] SendMainBuffer;
public NativeArray<byte> ReceiveMainBuffer;
public struct AckData
{
public ushort LastReceivedSequence;
public ushort ReceivedSequenceMask;
}
public AckData Ack;
public struct EntryData
{
public ulong NetworkObjectId;
public ushort BehaviourIndex;
public ushort VariableIndex;
public int TickWritten;
public ushort Position;
public ushort Length;
}
public NativeList<EntryData> Entries;
public struct SpawnData
{
public ulong NetworkObjectId;
public uint Hash;
public bool IsSceneObject;
public bool IsPlayerObject;
public ulong OwnerClientId;
public ulong ParentNetworkId;
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public int TickWritten;
}
public NativeList<SpawnData> Spawns;
public struct DespawnData
{
public ulong NetworkObjectId;
public int TickWritten;
}
public NativeList<DespawnData> Despawns;
public unsafe void Serialize(FastBufferWriter writer)
{
if (!writer.TryBeginWrite(
FastBufferWriter.GetWriteSize(CurrentTick) +
FastBufferWriter.GetWriteSize(Sequence) +
FastBufferWriter.GetWriteSize(Range) + Range +
FastBufferWriter.GetWriteSize(Ack) +
FastBufferWriter.GetWriteSize<ushort>() +
Entries.Length * sizeof(EntryData) +
FastBufferWriter.GetWriteSize<ushort>() +
Spawns.Length * sizeof(SpawnData) +
FastBufferWriter.GetWriteSize<ushort>() +
Despawns.Length * sizeof(DespawnData)
))
{
throw new OverflowException($"Not enough space to serialize {nameof(SnapshotDataMessage)}");
}
writer.WriteValue(CurrentTick);
writer.WriteValue(Sequence);
writer.WriteValue(Range);
writer.WriteBytes(SendMainBuffer, Range);
writer.WriteValue(Ack);
writer.WriteValue((ushort)Entries.Length);
writer.WriteBytes((byte*)Entries.GetUnsafePtr(), Entries.Length * sizeof(EntryData));
writer.WriteValue((ushort)Spawns.Length);
writer.WriteBytes((byte*)Spawns.GetUnsafePtr(), Spawns.Length * sizeof(SpawnData));
writer.WriteValue((ushort)Despawns.Length);
writer.WriteBytes((byte*)Despawns.GetUnsafePtr(), Despawns.Length * sizeof(DespawnData));
}
public unsafe bool Deserialize(FastBufferReader reader, ref NetworkContext context)
{
if (!reader.TryBeginRead(
FastBufferWriter.GetWriteSize(CurrentTick) +
FastBufferWriter.GetWriteSize(Sequence) +
FastBufferWriter.GetWriteSize(Range)
))
{
throw new OverflowException($"Not enough space to deserialize {nameof(SnapshotDataMessage)}");
}
reader.ReadValue(out CurrentTick);
reader.ReadValue(out Sequence);
reader.ReadValue(out Range);
ReceiveMainBuffer = new NativeArray<byte>(Range, Allocator.Temp);
reader.ReadBytesSafe((byte*)ReceiveMainBuffer.GetUnsafePtr(), Range);
reader.ReadValueSafe(out Ack);
reader.ReadValueSafe(out ushort length);
Entries = new NativeList<EntryData>(length, Allocator.Temp) { Length = length };
reader.ReadBytesSafe((byte*)Entries.GetUnsafePtr(), Entries.Length * sizeof(EntryData));
reader.ReadValueSafe(out length);
Spawns = new NativeList<SpawnData>(length, Allocator.Temp) { Length = length };
reader.ReadBytesSafe((byte*)Spawns.GetUnsafePtr(), Spawns.Length * sizeof(SpawnData));
reader.ReadValueSafe(out length);
Despawns = new NativeList<DespawnData>(length, Allocator.Temp) { Length = length };
reader.ReadBytesSafe((byte*)Despawns.GetUnsafePtr(), Despawns.Length * sizeof(DespawnData));
return true;
}
public void Handle(ref NetworkContext context)
{
using (ReceiveMainBuffer)
using (Entries)
using (Spawns)
using (Despawns)
{
var systemOwner = context.SystemOwner;
var senderId = context.SenderId;
if (systemOwner is NetworkManager networkManager)
{
// todo: temporary hack around bug
if (!networkManager.IsServer)
{
senderId = networkManager.ServerClientId;
}
var snapshotSystem = networkManager.SnapshotSystem;
snapshotSystem.HandleSnapshot(senderId, this);
}
else
{
var ownerData = (Tuple<SnapshotSystem, ulong>)systemOwner;
var snapshotSystem = ownerData.Item1;
snapshotSystem.HandleSnapshot(ownerData.Item2, this);
}
}
}
}
}

View File

@@ -67,7 +67,7 @@ namespace Unity.Netcode
}
public const int NON_FRAGMENTED_MESSAGE_MAX_SIZE = 1300;
public const int FRAGMENTED_MESSAGE_MAX_SIZE = int.MaxValue;
public const int FRAGMENTED_MESSAGE_MAX_SIZE = BytePacker.BitPackedIntMax;
internal struct MessageWithHandler
{

View File

@@ -87,7 +87,13 @@ namespace Unity.Netcode
void TrackPacketReceived(uint packetCount);
void TrackRttToServer(int rtt);
void UpdateRttToServer(int rtt);
void UpdateNetworkObjectsCount(int count);
void UpdateConnectionsCount(int count);
void UpdatePacketLoss(float packetLoss);
void DispatchFrame();
}

View File

@@ -66,7 +66,7 @@ namespace Unity.Netcode
private readonly EventMetric<SceneEventMetric> m_SceneEventSentEvent = new EventMetric<SceneEventMetric>(NetworkMetricTypes.SceneEventSent.Id);
private readonly EventMetric<SceneEventMetric> m_SceneEventReceivedEvent = new EventMetric<SceneEventMetric>(NetworkMetricTypes.SceneEventReceived.Id);
#if MULTIPLAYER_TOOLS_1_0_0_PRE_4
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
private readonly Counter m_PacketSentCounter = new Counter(NetworkMetricTypes.PacketsSent.Id)
{
ShouldResetOnDispatch = true,
@@ -79,6 +79,15 @@ namespace Unity.Netcode
{
ShouldResetOnDispatch = true,
};
private readonly Gauge m_NetworkObjectsGauge = new Gauge(NetworkMetricTypes.NetworkObjects.Id)
{
ShouldResetOnDispatch = true,
};
private readonly Gauge m_ConnectionsGauge = new Gauge(NetworkMetricTypes.ConnectedClients.Id)
{
ShouldResetOnDispatch = true,
};
private readonly Gauge m_PacketLossGauge = new Gauge(NetworkMetricTypes.PacketLoss.Id);
#endif
private ulong m_NumberOfMetricsThisFrame;
@@ -97,9 +106,12 @@ namespace Unity.Netcode
.WithMetricEvents(m_RpcSentEvent, m_RpcReceivedEvent)
.WithMetricEvents(m_ServerLogSentEvent, m_ServerLogReceivedEvent)
.WithMetricEvents(m_SceneEventSentEvent, m_SceneEventReceivedEvent)
#if MULTIPLAYER_TOOLS_1_0_0_PRE_4
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
.WithCounters(m_PacketSentCounter, m_PacketReceivedCounter)
.WithGauges(m_RttToServerGauge)
.WithGauges(m_NetworkObjectsGauge)
.WithGauges(m_ConnectionsGauge)
.WithGauges(m_PacketLossGauge)
#endif
.Build();
@@ -428,7 +440,7 @@ namespace Unity.Netcode
public void TrackPacketSent(uint packetCount)
{
#if MULTIPLAYER_TOOLS_1_0_0_PRE_4
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
if (!CanSendMetrics)
{
return;
@@ -441,7 +453,7 @@ namespace Unity.Netcode
public void TrackPacketReceived(uint packetCount)
{
#if MULTIPLAYER_TOOLS_1_0_0_PRE_4
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
if (!CanSendMetrics)
{
return;
@@ -452,15 +464,51 @@ namespace Unity.Netcode
#endif
}
public void TrackRttToServer(int rtt)
public void UpdateRttToServer(int rttMilliseconds)
{
#if MULTIPLAYER_TOOLS_1_0_0_PRE_4
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
if (!CanSendMetrics)
{
return;
}
var rttSeconds = rttMilliseconds * 1e-3;
m_RttToServerGauge.Set(rttSeconds);
#endif
}
public void UpdateNetworkObjectsCount(int count)
{
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
if (!CanSendMetrics)
{
return;
}
m_RttToServerGauge.Set(rtt);
m_NetworkObjectsGauge.Set(count);
#endif
}
public void UpdateConnectionsCount(int count)
{
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
if (!CanSendMetrics)
{
return;
}
m_ConnectionsGauge.Set(count);
#endif
}
public void UpdatePacketLoss(float packetLoss)
{
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
if (!CanSendMetrics)
{
return;
}
m_PacketLossGauge.Set(packetLoss);
#endif
}

View File

@@ -145,7 +145,19 @@ namespace Unity.Netcode
{
}
public void TrackRttToServer(int rtt)
public void UpdateRttToServer(int rtt)
{
}
public void UpdateNetworkObjectsCount(int count)
{
}
public void UpdateConnectionsCount(int count)
{
}
public void UpdatePacketLoss(float packetLoss)
{
}

View File

@@ -24,17 +24,12 @@ namespace Unity.Netcode
/// </summary>
public event OnListChangedDelegate OnListChanged;
/// <summary>
/// Creates a NetworkList with the default value and settings
/// </summary>
public NetworkList() { }
/// <summary>
/// Creates a NetworkList with the default value and custom settings
/// </summary>
/// <param name="readPerm">The read permission to use for the NetworkList</param>
/// <param name="values">The initial value to use for the NetworkList</param>
public NetworkList(NetworkVariableReadPermission readPerm, IEnumerable<T> values) : base(readPerm)
public NetworkList(IEnumerable<T> values = default,
NetworkVariableReadPermission readPerm = DefaultReadPerm,
NetworkVariableWritePermission writePerm = DefaultWritePerm)
: base(readPerm, writePerm)
{
foreach (var value in values)
{
@@ -42,19 +37,6 @@ namespace Unity.Netcode
}
}
/// <summary>
/// Creates a NetworkList with a custom value and the default settings
/// </summary>
/// <param name="values">The initial value to use for the NetworkList</param>
public NetworkList(IEnumerable<T> values)
{
foreach (var value in values)
{
m_List.Add(value);
}
}
/// <inheritdoc />
public override void ResetDirty()
{

View File

@@ -1,5 +1,7 @@
using UnityEngine;
using System;
using System.Runtime.CompilerServices;
using Unity.Collections.LowLevel.Unsafe;
namespace Unity.Netcode
{
@@ -22,7 +24,8 @@ namespace Unity.Netcode
}
// Functions that serialize other types
private static void WriteValue<TForMethod>(FastBufferWriter writer, in TForMethod value) where TForMethod : unmanaged
private static void WriteValue<TForMethod>(FastBufferWriter writer, in TForMethod value)
where TForMethod : unmanaged
{
writer.WriteValueSafe(value);
}
@@ -37,16 +40,13 @@ namespace Unity.Netcode
internal delegate void ReadDelegate<TForMethod>(FastBufferReader reader, out TForMethod value);
// These static delegates provide the right implementation for writing and reading a particular network variable
// type.
//
// These static delegates provide the right implementation for writing and reading a particular network variable type.
// For most types, these default to WriteValue() and ReadValue(), which perform simple memcpy operations.
//
// INetworkSerializableILPP will generate startup code that will set it to WriteNetworkSerializable()
// and ReadNetworkSerializable() for INetworkSerializable types, which will call NetworkSerialize().
//
// In the future we may be able to use this to provide packing implementations for floats and integers to
// optimize bandwidth usage.
// In the future we may be able to use this to provide packing implementations for floats and integers to optimize bandwidth usage.
//
// The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this,
// NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable,
@@ -69,38 +69,11 @@ namespace Unity.Netcode
/// </summary>
public OnValueChangedDelegate OnValueChanged;
/// <summary>
/// Creates a NetworkVariable with the default value and custom read permission
/// </summary>
/// <param name="readPerm">The read permission for the NetworkVariable</param>
public NetworkVariable()
{
}
/// <summary>
/// Creates a NetworkVariable with the default value and custom read permission
/// </summary>
/// <param name="readPerm">The read permission for the NetworkVariable</param>
public NetworkVariable(NetworkVariableReadPermission readPerm) : base(readPerm)
{
}
/// <summary>
/// Creates a NetworkVariable with a custom value and custom settings
/// </summary>
/// <param name="readPerm">The read permission for the NetworkVariable</param>
/// <param name="value">The initial value to use for the NetworkVariable</param>
public NetworkVariable(NetworkVariableReadPermission readPerm, T value) : base(readPerm)
{
m_InternalValue = value;
}
/// <summary>
/// Creates a NetworkVariable with a custom value and the default read permission
/// </summary>
/// <param name="value">The initial value to use for the NetworkVariable</param>
public NetworkVariable(T value)
public NetworkVariable(T value = default,
NetworkVariableReadPermission readPerm = DefaultReadPerm,
NetworkVariableWritePermission writePerm = DefaultWritePerm)
: base(readPerm, writePerm)
{
m_InternalValue = value;
}
@@ -116,19 +89,36 @@ namespace Unity.Netcode
get => m_InternalValue;
set
{
// this could be improved. The Networking Manager is not always initialized here
// Good place to decouple network manager from the network variable
// Also, note this is not really very water-tight, if you are running as a host
// we cannot tell if a NetworkVariable write is happening inside client-ish code
if (m_NetworkBehaviour && (m_NetworkBehaviour.NetworkManager.IsClient && !m_NetworkBehaviour.NetworkManager.IsHost))
// Compare bitwise
if (ValueEquals(ref m_InternalValue, ref value))
{
throw new InvalidOperationException("Client can't write to NetworkVariables");
return;
}
if (m_NetworkBehaviour && !CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId))
{
throw new InvalidOperationException("Client is not allowed to write to this NetworkVariable");
}
Set(value);
}
}
// Compares two values of the same unmanaged type by underlying memory
// Ignoring any overriden value checks
// Size is fixed
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe bool ValueEquals(ref T a, ref T b)
{
// get unmanaged pointers
var aptr = UnsafeUtility.AddressOf(ref a);
var bptr = UnsafeUtility.AddressOf(ref b);
// compare addresses
return UnsafeUtility.MemCmp(aptr, bptr, sizeof(T)) == 0;
}
private protected void Set(T value)
{
m_IsDirty = true;
@@ -146,7 +136,6 @@ namespace Unity.Netcode
WriteField(writer);
}
/// <summary>
/// Reads value from the reader and applies it
/// </summary>

View File

@@ -19,9 +19,15 @@ namespace Unity.Netcode
m_NetworkBehaviour = networkBehaviour;
}
protected NetworkVariableBase(NetworkVariableReadPermission readPermIn = NetworkVariableReadPermission.Everyone)
public const NetworkVariableReadPermission DefaultReadPerm = NetworkVariableReadPermission.Everyone;
public const NetworkVariableWritePermission DefaultWritePerm = NetworkVariableWritePermission.Server;
protected NetworkVariableBase(
NetworkVariableReadPermission readPerm = DefaultReadPerm,
NetworkVariableWritePermission writePerm = DefaultWritePerm)
{
ReadPerm = readPermIn;
ReadPerm = readPerm;
WritePerm = writePerm;
}
private protected bool m_IsDirty;
@@ -37,6 +43,8 @@ namespace Unity.Netcode
/// </summary>
public readonly NetworkVariableReadPermission ReadPerm;
public readonly NetworkVariableWritePermission WritePerm;
/// <summary>
/// Sets whether or not the variable needs to be delta synced
/// </summary>
@@ -62,26 +70,28 @@ namespace Unity.Netcode
return m_IsDirty;
}
public virtual bool ShouldWrite(ulong clientId, bool isServer)
{
return IsDirty() && isServer && CanClientRead(clientId);
}
/// <summary>
/// Gets Whether or not a specific client can read to the varaible
/// </summary>
/// <param name="clientId">The clientId of the remote client</param>
/// <returns>Whether or not the client can read to the variable</returns>
public bool CanClientRead(ulong clientId)
{
switch (ReadPerm)
{
default:
case NetworkVariableReadPermission.Everyone:
return true;
case NetworkVariableReadPermission.OwnerOnly:
return m_NetworkBehaviour.OwnerClientId == clientId;
case NetworkVariableReadPermission.Owner:
return clientId == m_NetworkBehaviour.NetworkObject.OwnerClientId;
}
}
public bool CanClientWrite(ulong clientId)
{
switch (WritePerm)
{
default:
case NetworkVariableWritePermission.Server:
return clientId == NetworkManager.ServerClientId;
case NetworkVariableWritePermission.Owner:
return clientId == m_NetworkBehaviour.NetworkObject.OwnerClientId;
}
return true;
}
/// <summary>
@@ -107,7 +117,6 @@ namespace Unity.Netcode
/// </summary>
/// <param name="reader">The stream to read the delta from</param>
/// <param name="keepDirtyDelta">Whether or not the delta should be kept as dirty or consumed</param>
public abstract void ReadDelta(FastBufferReader reader, bool keepDirtyDelta);
public virtual void Dispose()

View File

@@ -1,18 +1,14 @@
namespace Unity.Netcode
{
/// <summary>
/// Permission type
/// </summary>
public enum NetworkVariableReadPermission
{
/// <summary>
/// Everyone
/// </summary>
Everyone,
Owner,
}
/// <summary>
/// Owner-ownly
/// </summary>
OwnerOnly,
public enum NetworkVariableWritePermission
{
Server,
Owner
}
}

View File

@@ -132,7 +132,7 @@ namespace Unity.Netcode
private const NetworkDelivery k_DeliveryType = NetworkDelivery.ReliableFragmentedSequenced;
internal const int InvalidSceneNameOrPath = -1;
// Used to be able to turn re-synchronization off for future snapshot development purposes.
// Used to be able to turn re-synchronization off
internal static bool DisableReSynchronization;
/// <summary>
@@ -488,8 +488,18 @@ namespace Unity.Netcode
var scenePath = SceneUtility.GetScenePathByBuildIndex(i);
var hash = XXHash.Hash32(scenePath);
var buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath);
HashToBuildIndex.Add(hash, buildIndex);
BuildIndexToHash.Add(buildIndex, hash);
// In the rare-case scenario where a programmatically generated build has duplicate
// scene entries, we will log an error and skip the entry
if (!HashToBuildIndex.ContainsKey(hash))
{
HashToBuildIndex.Add(hash, buildIndex);
BuildIndexToHash.Add(buildIndex, hash);
}
else
{
Debug.LogError($"{nameof(NetworkSceneManager)} is skipping duplicate scene path entry {scenePath}. Make sure your scenes in build list does not contain duplicates!");
}
}
}
@@ -520,7 +530,8 @@ namespace Unity.Netcode
}
else
{
throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table!");
throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table! Verify that all scenes requiring" +
$" server to client synchronization are in the scenes in build list.");
}
}
@@ -876,7 +887,7 @@ namespace Unity.Netcode
{
SceneEventType = sceneEventProgress.SceneEventType,
SceneName = SceneNameFromHash(sceneEventProgress.SceneHash),
ClientId = m_NetworkManager.ServerClientId,
ClientId = NetworkManager.ServerClientId,
LoadSceneMode = sceneEventProgress.LoadSceneMode,
ClientsThatCompleted = sceneEventProgress.DoneClients,
ClientsThatTimedOut = m_NetworkManager.ConnectedClients.Keys.Except(sceneEventProgress.DoneClients).ToList(),
@@ -947,10 +958,10 @@ namespace Unity.Netcode
SceneEventType = sceneEventData.SceneEventType,
LoadSceneMode = sceneEventData.LoadSceneMode,
SceneName = sceneName,
ClientId = m_NetworkManager.ServerClientId // Server can only invoke this
ClientId = NetworkManager.ServerClientId // Server can only invoke this
});
OnUnload?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneUnload);
OnUnload?.Invoke(NetworkManager.ServerClientId, sceneName, sceneUnload);
//Return the status
return sceneEventProgress.Status;
@@ -1017,12 +1028,12 @@ namespace Unity.Netcode
// Server sends the unload scene notification after unloading because it will despawn all scene relative in-scene NetworkObjects
// If we send this event to all clients before the server is finished unloading they will get warning about an object being
// despawned that no longer exists
SendSceneEventData(sceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != m_NetworkManager.ServerClientId).ToArray());
SendSceneEventData(sceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray());
//Only if we are a host do we want register having loaded for the associated SceneEventProgress
if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost)
{
SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId);
SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(NetworkManager.ServerClientId);
}
}
@@ -1035,7 +1046,7 @@ namespace Unity.Netcode
SceneEventType = sceneEventData.SceneEventType,
LoadSceneMode = sceneEventData.LoadSceneMode,
SceneName = SceneNameFromHash(sceneEventData.SceneHash),
ClientId = m_NetworkManager.IsServer ? m_NetworkManager.ServerClientId : m_NetworkManager.LocalClientId
ClientId = m_NetworkManager.IsServer ? NetworkManager.ServerClientId : m_NetworkManager.LocalClientId
});
OnUnloadComplete?.Invoke(m_NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash));
@@ -1043,7 +1054,7 @@ namespace Unity.Netcode
// Clients send a notification back to the server they have completed the unload scene event
if (!m_NetworkManager.IsServer)
{
SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId });
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
}
EndSceneEvent(sceneEventId);
@@ -1079,7 +1090,7 @@ namespace Unity.Netcode
SceneEventType = SceneEventType.Unload,
SceneName = keyHandleEntry.Value.name,
LoadSceneMode = LoadSceneMode.Additive, // The only scenes unloaded are scenes that were additively loaded
ClientId = m_NetworkManager.ServerClientId
ClientId = NetworkManager.ServerClientId
});
}
}
@@ -1147,10 +1158,10 @@ namespace Unity.Netcode
SceneEventType = sceneEventData.SceneEventType,
LoadSceneMode = sceneEventData.LoadSceneMode,
SceneName = sceneName,
ClientId = m_NetworkManager.ServerClientId
ClientId = NetworkManager.ServerClientId
});
OnLoad?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad);
OnLoad?.Invoke(NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad);
//Return our scene progress instance
return sceneEventProgress.Status;
@@ -1187,7 +1198,6 @@ namespace Unity.Netcode
// When it is set: Just before starting the asynchronous loading call
// When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do
// not destroy temporary scene are moved into the active scene
// TODO: When Snapshot scene spawning is enabled this needs to be removed.
if (sceneEventData.LoadSceneMode == LoadSceneMode.Single)
{
IsSpawnedObjectsPendingInDontDestroyOnLoad = true;
@@ -1278,7 +1288,9 @@ namespace Unity.Netcode
{
if (!keyValuePairBySceneHandle.Value.IsPlayerObject)
{
m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value, m_NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, null, true);
// All in-scene placed NetworkObjects default to being owned by the server
m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value,
m_NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, NetworkManager.ServerClientId, true);
}
}
}
@@ -1290,7 +1302,7 @@ namespace Unity.Netcode
for (int j = 0; j < m_NetworkManager.ConnectedClientsList.Count; j++)
{
var clientId = m_NetworkManager.ConnectedClientsList[j].ClientId;
if (clientId != m_NetworkManager.ServerClientId)
if (clientId != NetworkManager.ServerClientId)
{
sceneEventData.TargetClientId = clientId;
var message = new SceneEventMessage
@@ -1309,16 +1321,16 @@ namespace Unity.Netcode
SceneEventType = SceneEventType.LoadComplete,
LoadSceneMode = sceneEventData.LoadSceneMode,
SceneName = SceneNameFromHash(sceneEventData.SceneHash),
ClientId = m_NetworkManager.ServerClientId,
ClientId = NetworkManager.ServerClientId,
Scene = scene,
});
OnLoadComplete?.Invoke(m_NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode);
OnLoadComplete?.Invoke(NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode);
//Second, only if we are a host do we want register having loaded for the associated SceneEventProgress
if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost)
{
SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId);
SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(NetworkManager.ServerClientId);
}
EndSceneEvent(sceneEventId);
}
@@ -1333,7 +1345,7 @@ namespace Unity.Netcode
sceneEventData.DeserializeScenePlacedObjects();
sceneEventData.SceneEventType = SceneEventType.LoadComplete;
SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId });
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
m_IsSceneEventActive = false;
// Notify local client that the scene was loaded
@@ -1544,9 +1556,9 @@ namespace Unity.Netcode
{
EventData = responseSceneEventData
};
var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, m_NetworkManager.ServerClientId);
var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId);
m_NetworkManager.NetworkMetrics.TrackSceneEventSent(m_NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size);
m_NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size);
EndSceneEvent(responseSceneEventData.SceneEventId);
@@ -1600,7 +1612,7 @@ namespace Unity.Netcode
sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager);
sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete;
SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId });
SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId });
// All scenes are synchronized, let the server know we are done synchronizing
m_NetworkManager.IsConnectedClient = true;
@@ -1627,7 +1639,7 @@ namespace Unity.Netcode
OnSceneEvent?.Invoke(new SceneEvent()
{
SceneEventType = sceneEventData.SceneEventType,
ClientId = m_NetworkManager.ServerClientId, // Server sent this to client
ClientId = NetworkManager.ServerClientId, // Server sent this to client
});
EndSceneEvent(sceneEventId);
@@ -1642,7 +1654,7 @@ namespace Unity.Netcode
SceneEventType = sceneEventData.SceneEventType,
LoadSceneMode = sceneEventData.LoadSceneMode,
SceneName = SceneNameFromHash(sceneEventData.SceneHash),
ClientId = m_NetworkManager.ServerClientId,
ClientId = NetworkManager.ServerClientId,
ClientsThatCompleted = sceneEventData.ClientsCompleted,
ClientsThatTimedOut = sceneEventData.ClientsTimedOut,
});
@@ -1734,8 +1746,6 @@ namespace Unity.Netcode
// NetworkObjects
m_NetworkManager.InvokeOnClientConnectedCallback(clientId);
// TODO: This check and associated code can be removed once we determine all
// snapshot destroy messages are being updated until the server receives ACKs
if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization)
{
sceneEventData.SceneEventType = SceneEventType.ReSynchronize;
@@ -1830,7 +1840,7 @@ namespace Unity.Netcode
/// Using the local scene relative Scene.handle as a sub-key to the root dictionary allows us to
/// distinguish between duplicate in-scene placed NetworkObjects
/// </summary>
private void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true)
internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true)
{
if (clearScenePlacedObjects)
{
@@ -1845,25 +1855,26 @@ namespace Unity.Netcode
// at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects
foreach (var networkObjectInstance in networkObjects)
{
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (additive scenes)
if (networkObjectInstance.IsSceneObject == null && networkObjectInstance.NetworkManager == m_NetworkManager && networkObjectInstance.gameObject.scene == sceneToFilterBy &&
networkObjectInstance.gameObject.scene.handle == sceneToFilterBy.handle)
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
var sceneHandle = networkObjectInstance.gameObject.scene.handle;
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && networkObjectInstance.gameObject.scene == sceneToFilterBy &&
sceneHandle == sceneToFilterBy.handle)
{
if (!ScenePlacedObjects.ContainsKey(networkObjectInstance.GlobalObjectIdHash))
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
ScenePlacedObjects.Add(networkObjectInstance.GlobalObjectIdHash, new Dictionary<int, NetworkObject>());
ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary<int, NetworkObject>());
}
if (!ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash].ContainsKey(networkObjectInstance.gameObject.scene.handle))
if (!ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle))
{
ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash].Add(networkObjectInstance.gameObject.scene.handle, networkObjectInstance);
ScenePlacedObjects[globalObjectIdHash].Add(sceneHandle, networkObjectInstance);
}
else
{
var exitingEntryName = ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash][networkObjectInstance.gameObject.scene.handle] != null ?
ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash][networkObjectInstance.gameObject.scene.handle].name : "Null Entry";
var exitingEntryName = ScenePlacedObjects[globalObjectIdHash][sceneHandle] != null ? ScenePlacedObjects[globalObjectIdHash][sceneHandle].name : "Null Entry";
throw new Exception($"{networkObjectInstance.name} tried to registered with {nameof(ScenePlacedObjects)} which already contains " +
$"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {networkObjectInstance.GlobalObjectIdHash} for {exitingEntryName}!");
$"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!");
}
}
}

View File

@@ -41,7 +41,6 @@ namespace Unity.Netcode
/// <b>Invocation:</b> Server Side<br/>
/// <b>Message Flow:</b> Server to client<br/>
/// <b>Event Notification:</b> Both server and client receive a local notification<br/>
/// <em>Note: This will be removed once snapshot and buffered messages are finalized as it will no longer be needed at that point.</em>
/// </summary>
ReSynchronize,
/// <summary>
@@ -592,9 +591,6 @@ namespace Unity.Netcode
networkObject.IsSpawned = false;
if (m_NetworkManager.PrefabHandler.ContainsHandler(networkObject))
{
// Since this is the client side and we have missed the delete message, until the Snapshot system is in place for spawn and despawn handling
// we have to remove this from the list of spawned objects manually or when a NetworkObjectId is recycled the client will throw an error
// about the id already being assigned.
if (m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
{
m_NetworkManager.SpawnManager.SpawnedObjects.Remove(networkObjectId);

View File

@@ -10,7 +10,7 @@ namespace Unity.Netcode
public static class BytePacker
{
#if UNITY_NETCODE_DEBUG_NO_PACKING
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteValuePacked<T>(FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value);
#else
@@ -277,10 +277,21 @@ namespace Unity.Netcode
#if UNITY_NETCODE_DEBUG_NO_PACKING
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteValueBitPacked<T>(FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value);
#else
public const ushort BitPackedUshortMax = (1 << 15) - 1;
public const short BitPackedShortMax = (1 << 14) - 1;
public const short BitPackedShortMin = -(1 << 14);
public const uint BitPackedUintMax = (1 << 30) - 1;
public const int BitPackedIntMax = (1 << 29) - 1;
public const int BitPackedIntMin = -(1 << 29);
public const ulong BitPackedULongMax = (1L << 61) - 1;
public const long BitPackedLongMax = (1L << 60) - 1;
public const long BitPackedLongMin = -(1L << 60);
/// <summary>
/// Writes a 14-bit signed short to the buffer in a bit-encoded packed format.
/// The first bit indicates whether the value is 1 byte or 2.
@@ -307,7 +318,7 @@ namespace Unity.Netcode
public static void WriteValueBitPacked(FastBufferWriter writer, ushort value)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (value >= 0b1000_0000_0000_0000)
if (value >= BitPackedUshortMax)
{
throw new ArgumentException("BitPacked ushorts must be <= 15 bits");
}
@@ -356,7 +367,7 @@ namespace Unity.Netcode
public static void WriteValueBitPacked(FastBufferWriter writer, uint value)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (value >= 0b0100_0000_0000_0000_0000_0000_0000_0000)
if (value > BitPackedUintMax)
{
throw new ArgumentException("BitPacked uints must be <= 30 bits");
}
@@ -396,7 +407,7 @@ namespace Unity.Netcode
public static void WriteValueBitPacked(FastBufferWriter writer, ulong value)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (value >= 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000)
if (value > BitPackedULongMax)
{
throw new ArgumentException("BitPacked ulongs must be <= 61 bits");
}

View File

@@ -19,7 +19,7 @@ namespace Unity.Netcode
#endif
}
internal readonly unsafe ReaderHandle* Handle;
internal unsafe ReaderHandle* Handle;
/// <summary>
/// Get the current read position
@@ -39,6 +39,11 @@ namespace Unity.Netcode
get => Handle->Length;
}
/// <summary>
/// Gets a value indicating whether the reader has been initialized and a handle allocated.
/// </summary>
public unsafe bool IsInitialized => Handle != null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe void CommitBitwiseReads(int amount)
{
@@ -196,6 +201,7 @@ namespace Unity.Netcode
public unsafe void Dispose()
{
UnsafeUtility.Free(Handle, Handle->Allocator);
Handle = null;
}
/// <summary>

View File

@@ -22,7 +22,7 @@ namespace Unity.Netcode
#endif
}
internal readonly unsafe WriterHandle* Handle;
internal unsafe WriterHandle* Handle;
private static byte[] s_ByteArrayCache = new byte[65535];
@@ -62,6 +62,11 @@ namespace Unity.Netcode
get => Handle->Position > Handle->Length ? Handle->Position : Handle->Length;
}
/// <summary>
/// Gets a value indicating whether the writer has been initialized and a handle allocated.
/// </summary>
public unsafe bool IsInitialized => Handle != null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe void CommitBitwiseWrites(int amount)
@@ -111,6 +116,7 @@ namespace Unity.Netcode
UnsafeUtility.Free(Handle->BufferPointer, Handle->Allocator);
}
UnsafeUtility.Free(Handle, Handle->Allocator);
Handle = null;
}
/// <summary>
@@ -207,7 +213,7 @@ namespace Unity.Netcode
/// When you know you will be writing multiple fields back-to-back and you know the total size,
/// you can call TryBeginWrite() once on the total size, and then follow it with calls to
/// WriteValue() instead of WriteValueSafe() for faster serialization.
///
///
/// Unsafe write operations will throw OverflowException in editor and development builds if you
/// go past the point you've marked using TryBeginWrite(). In release builds, OverflowException will not be thrown
/// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following
@@ -253,7 +259,7 @@ namespace Unity.Netcode
/// When you know you will be writing multiple fields back-to-back and you know the total size,
/// you can call TryBeginWrite() once on the total size, and then follow it with calls to
/// WriteValue() instead of WriteValueSafe() for faster serialization.
///
///
/// Unsafe write operations will throw OverflowException in editor and development builds if you
/// go past the point you've marked using TryBeginWrite(). In release builds, OverflowException will not be thrown
/// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following

View File

@@ -21,6 +21,122 @@ namespace Unity.Netcode
/// </summary>
public readonly HashSet<NetworkObject> SpawnedObjectsList = new HashSet<NetworkObject>();
/// <summary>
/// Use to get all NetworkObjects owned by a client
/// Ownership to Objects Table Format:
/// [ClientId][NetworkObjectId][NetworkObject]
/// Server: Keeps track of all clients' ownership
/// Client: Keeps track of only its ownership
/// </summary>
public readonly Dictionary<ulong, Dictionary<ulong, NetworkObject>> OwnershipToObjectsTable = new Dictionary<ulong, Dictionary<ulong, NetworkObject>>();
/// <summary>
/// Object to Ownership Table:
/// [NetworkObjectId][ClientId]
/// Used internally to find the client Id that currently owns
/// the NetworkObject
/// </summary>
private Dictionary<ulong, ulong> m_ObjectToOwnershipTable = new Dictionary<ulong, ulong>();
/// <summary>
/// Used to update a NetworkObject's ownership
/// </summary>
internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false)
{
var previousOwner = newOwner;
// Use internal lookup table to see if the NetworkObject has a previous owner
if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId))
{
// Keep track of the previous owner's ClientId
previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId];
// We are either despawning (remove) or changing ownership (assign)
if (isRemoving)
{
m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId);
}
else
{
m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner;
}
}
else
{
// Otherwise, just add a new lookup entry
m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner);
}
// Check to see if we had a previous owner
if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner))
{
// Before updating the previous owner, assure this entry exists
if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId))
{
// Remove the previous owner's entry
OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId);
// Server or Host alway invokes the lost ownership notification locally
if (NetworkManager.IsServer)
{
networkObject.InvokeBehaviourOnLostOwnership();
}
// If we are removing the entry (i.e. despawning or client lost ownership)
if (isRemoving)
{
return;
}
}
else
{
// Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen
throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?");
}
}
// If the owner doesn't have an entry then create one
if (!OwnershipToObjectsTable.ContainsKey(newOwner))
{
OwnershipToObjectsTable.Add(newOwner, new Dictionary<ulong, NetworkObject>());
}
// Sanity check to make sure we don't already have this entry (we shouldn't)
if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId))
{
// Add the new ownership entry
OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject);
// Server or Host always invokes the gained ownership notification locally
if (NetworkManager.IsServer)
{
networkObject.InvokeBehaviourOnGainedOwnership();
}
}
else if (isRemoving)
{
OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId);
}
else if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!");
}
}
/// <summary>
/// Returns a list of all NetworkObjects that belong to a client.
/// </summary>
/// <param name="clientId">the client's id <see cref="NetworkManager.LocalClientId"/></param>
public List<NetworkObject> GetClientOwnedObjects(ulong clientId)
{
if (!OwnershipToObjectsTable.ContainsKey(clientId))
{
OwnershipToObjectsTable.Add(clientId, new Dictionary<ulong, NetworkObject>());
}
return OwnershipToObjectsTable[clientId].Values.ToList();
}
private struct TriggerData
{
public FastBufferReader Reader;
@@ -96,8 +212,7 @@ namespace Unity.Netcode
/// <summary>
/// Defers processing of a message until the moment a specific networkObjectId is spawned.
/// This is to handle situations where an RPC or other object-specific message arrives before the spawn does,
/// either due to it being requested in OnNetworkSpawn before the spawn call has been executed, or with
/// snapshot spawns enabled where the spawn is sent unreliably and not until the end of the frame.
/// either due to it being requested in OnNetworkSpawn before the spawn call has been executed
///
/// There is a one second maximum lifetime of triggers to avoid memory leaks. After one second has passed
/// without the requested object ID being spawned, the triggers for it are automatically deleted.
@@ -194,37 +309,21 @@ namespace Unity.Netcode
return;
}
// Make sure the connected client entry exists before trying to remove ownership.
if (TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient))
// Server removes the entry and takes over ownership before notifying
UpdateOwnershipTable(networkObject, NetworkManager.ServerClientId, true);
networkObject.OwnerClientId = NetworkManager.ServerClientId;
var message = new ChangeOwnershipMessage
{
for (int i = networkClient.OwnedObjects.Count - 1; i > -1; i--)
{
if (networkClient.OwnedObjects[i] == networkObject)
{
networkClient.OwnedObjects.RemoveAt(i);
}
}
NetworkObjectId = networkObject.NetworkObjectId,
OwnerClientId = networkObject.OwnerClientId
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds);
networkObject.OwnerClientIdInternal = null;
var message = new ChangeOwnershipMessage
{
NetworkObjectId = networkObject.NetworkObjectId,
OwnerClientId = networkObject.OwnerClientId
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds);
foreach (var client in NetworkManager.ConnectedClients)
{
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size);
}
}
else
foreach (var client in NetworkManager.ConnectedClients)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"No connected clients prior to removing ownership for {networkObject.name}. Make sure you are not initializing or shutting down when removing ownership.");
}
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size);
}
}
@@ -265,25 +364,10 @@ namespace Unity.Netcode
throw new SpawnStateException("Object is not spawned");
}
if (TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient))
{
for (int i = networkClient.OwnedObjects.Count - 1; i >= 0; i--)
{
if (networkClient.OwnedObjects[i] == networkObject)
{
networkClient.OwnedObjects.RemoveAt(i);
}
}
networkClient.OwnedObjects.Add(networkObject);
}
networkObject.OwnerClientId = clientId;
if (TryGetNetworkClient(clientId, out NetworkClient newNetworkClient))
{
newNetworkClient.OwnedObjects.Add(networkObject);
}
// Server adds entries for all client ownership
UpdateOwnershipTable(networkObject, networkObject.OwnerClientId);
var message = new ChangeOwnershipMessage
{
@@ -414,7 +498,7 @@ namespace Unity.Netcode
}
// Ran on both server and client
internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong? ownerClientId, bool destroyWithScene)
internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene)
{
if (networkObject == null)
{
@@ -452,15 +536,12 @@ namespace Unity.Netcode
throw new SpawnStateException("Object is already spawned");
}
if (sceneObject.Header.HasNetworkVariables)
{
networkObject.SetNetworkVariableData(variableData);
}
networkObject.SetNetworkVariableData(variableData);
SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.Header.NetworkObjectId, sceneObject.Header.IsSceneObject, sceneObject.Header.IsPlayerObject, sceneObject.Header.OwnerClientId, destroyWithScene);
}
private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong? ownerClientId, bool destroyWithScene)
private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene)
{
if (SpawnedObjects.ContainsKey(networkId))
{
@@ -471,38 +552,45 @@ namespace Unity.Netcode
// this initialization really should be at the bottom of the function
networkObject.IsSpawned = true;
// this initialization really should be at the top of this function. If and when we break the
// this initialization really should be at the top of this function. If and when we break the
// NetworkVariable dependency on NetworkBehaviour, this otherwise creates problems because
// SetNetworkVariableData above calls InitializeVariables, and the 'baked out' data isn't ready there;
// the current design banks on getting the network behaviour set and then only reading from it
// after the below initialization code. However cowardice compels me to hold off on moving this until
// that commit
// the current design banks on getting the network behaviour set and then only reading from it after the
// below initialization code. However cowardice compels me to hold off on moving this until that commit
networkObject.IsSceneObject = sceneObject;
networkObject.NetworkObjectId = networkId;
networkObject.DestroyWithScene = sceneObject || destroyWithScene;
networkObject.OwnerClientIdInternal = ownerClientId;
networkObject.OwnerClientId = ownerClientId;
networkObject.IsPlayerObject = playerObject;
SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject);
SpawnedObjectsList.Add(networkObject);
if (ownerClientId != null)
if (NetworkManager.IsServer)
{
if (NetworkManager.IsServer)
if (playerObject)
{
if (playerObject)
// If there was an already existing player object for this player, then mark it as no longer
// a player object.
if (NetworkManager.ConnectedClients[ownerClientId].PlayerObject != null)
{
NetworkManager.ConnectedClients[ownerClientId.Value].PlayerObject = networkObject;
}
else
{
NetworkManager.ConnectedClients[ownerClientId.Value].OwnedObjects.Add(networkObject);
NetworkManager.ConnectedClients[ownerClientId].PlayerObject.IsPlayerObject = false;
}
NetworkManager.ConnectedClients[ownerClientId].PlayerObject = networkObject;
}
else if (playerObject && ownerClientId.Value == NetworkManager.LocalClientId)
}
else if (ownerClientId == NetworkManager.LocalClientId)
{
if (playerObject)
{
// If there was an already existing player object for this player, then mark it as no longer a player object.
if (NetworkManager.LocalClient.PlayerObject != null)
{
NetworkManager.LocalClient.PlayerObject.IsPlayerObject = false;
}
NetworkManager.LocalClient.PlayerObject = networkObject;
}
}
@@ -549,25 +637,21 @@ namespace Unity.Netcode
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
{
if (!NetworkManager.NetworkConfig.UseSnapshotSpawn)
//Currently, if this is called and the clientId (destination) is the server's client Id, this case will be checked
// within the below Send function. To avoid unwarranted allocation of a PooledNetworkBuffer placing this check here. [NSS]
if (NetworkManager.IsServer && clientId == NetworkManager.ServerClientId)
{
//Currently, if this is called and the clientId (destination) is the server's client Id, this case
//will be checked within the below Send function. To avoid unwarranted allocation of a PooledNetworkBuffer
//placing this check here. [NSS]
if (NetworkManager.IsServer && clientId == NetworkManager.ServerClientId)
{
return;
}
var message = new CreateObjectMessage
{
ObjectInfo = networkObject.GetMessageSceneObject(clientId)
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId);
NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size);
networkObject.MarkVariablesDirty();
return;
}
var message = new CreateObjectMessage
{
ObjectInfo = networkObject.GetMessageSceneObject(clientId)
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId);
NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size);
networkObject.MarkVariablesDirty();
}
internal ulong? GetSpawnParentId(NetworkObject networkObject)
@@ -605,14 +689,12 @@ namespace Unity.Netcode
// Makes scene objects ready to be reused
internal void ServerResetShudownStateForSceneObjects()
{
foreach (var sobj in SpawnedObjectsList)
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSceneObject != null && c.IsSceneObject == true);
foreach (var sobj in networkObjects)
{
if ((sobj.IsSceneObject != null && sobj.IsSceneObject == true) || sobj.DestroyWithScene)
{
sobj.IsSpawned = false;
sobj.DestroyWithScene = false;
sobj.IsSceneObject = null;
}
sobj.IsSpawned = false;
sobj.DestroyWithScene = false;
sobj.IsSceneObject = null;
}
}
@@ -653,14 +735,12 @@ namespace Unity.Netcode
else if (networkObjects[i].IsSpawned)
{
// If it is an in-scene placed NetworkObject then just despawn
// and let it be destroyed when the scene is unloaded. Otherwise,
// despawn and destroy it.
var shouldDestroy = !(networkObjects[i].IsSceneObject != null
&& networkObjects[i].IsSceneObject.Value);
// and let it be destroyed when the scene is unloaded. Otherwise, despawn and destroy it.
var shouldDestroy = !(networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value);
OnDespawnObject(networkObjects[i], shouldDestroy);
}
else
else if (networkObjects[i].IsSceneObject != null && !networkObjects[i].IsSceneObject.Value)
{
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
}
@@ -711,9 +791,10 @@ namespace Unity.Netcode
}
}
foreach (var networkObject in networkObjectsToSpawn)
{
SpawnNetworkObjectLocally(networkObject, GetNetworkObjectId(), true, false, null, true);
SpawnNetworkObjectLocally(networkObject, GetNetworkObjectId(), true, false, networkObject.OwnerClientId, true);
}
}
@@ -757,18 +838,6 @@ namespace Unity.Netcode
}
}
if (!networkObject.IsOwnedByServer && !networkObject.IsPlayerObject && TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient))
{
//Someone owns it.
for (int i = networkClient.OwnedObjects.Count - 1; i > -1; i--)
{
if (networkClient.OwnedObjects[i].NetworkObjectId == networkObject.NetworkObjectId)
{
networkClient.OwnedObjects.RemoveAt(i);
}
}
}
networkObject.InvokeBehaviourNetworkDespawn();
if (NetworkManager != null && NetworkManager.IsServer)
@@ -782,38 +851,31 @@ namespace Unity.Netcode
});
}
if (NetworkManager.NetworkConfig.UseSnapshotSpawn)
if (networkObject != null)
{
networkObject.SnapshotDespawn();
}
else
{
if (networkObject != null)
// As long as we have any remaining clients, then notify of the object being destroy.
if (NetworkManager.ConnectedClientsList.Count > 0)
{
// As long as we have any remaining clients, then notify of the object being destroy.
if (NetworkManager.ConnectedClientsList.Count > 0)
m_TargetClientIds.Clear();
// We keep only the client for which the object is visible
// as the other clients have them already despawned
foreach (var clientId in NetworkManager.ConnectedClientsIds)
{
m_TargetClientIds.Clear();
// We keep only the client for which the object is visible
// as the other clients have them already despawned
foreach (var clientId in NetworkManager.ConnectedClientsIds)
if (networkObject.IsNetworkVisibleTo(clientId))
{
if (networkObject.IsNetworkVisibleTo(clientId))
{
m_TargetClientIds.Add(clientId);
}
m_TargetClientIds.Add(clientId);
}
}
var message = new DestroyObjectMessage
{
NetworkObjectId = networkObject.NetworkObjectId
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, m_TargetClientIds);
foreach (var targetClientId in m_TargetClientIds)
{
NetworkManager.NetworkMetrics.TrackObjectDestroySent(targetClientId, networkObject, size);
}
var message = new DestroyObjectMessage
{
NetworkObjectId = networkObject.NetworkObjectId
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, m_TargetClientIds);
foreach (var targetClientId in m_TargetClientIds)
{
NetworkManager.NetworkMetrics.TrackObjectDestroySent(targetClientId, networkObject, size);
}
}
}

View File

@@ -1,54 +0,0 @@
#if UNITY_UNET_PRESENT
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Networking;
namespace Unity.Netcode.Transports.UNET
{
/// <summary>
/// A transport channel used by the netcode
/// </summary>
[Serializable]
public class UNetChannel
{
/// <summary>
/// The name of the channel
/// </summary>
#if UNITY_EDITOR
[ReadOnly]
#endif
public byte Id;
/// <summary>
/// The type of channel
/// </summary>
public QosType Type;
#if UNITY_EDITOR
private class ReadOnlyAttribute : PropertyAttribute { }
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
private class ReadOnlyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Saving previous GUI enabled value
var previousGUIState = GUI.enabled;
// Disabling edit for property
GUI.enabled = false;
// Drawing Property
EditorGUI.PropertyField(position, property, label);
// Setting old GUI enabled value
GUI.enabled = previousGUIState;
}
}
#endif
}
}
#endif

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e864534da30ef604992c0ed33c75d3c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 81887adf6d9ca40c9b70728b7018b6f5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,96 @@
using System;
using Unity.Networking.Transport;
namespace Unity.Netcode.Transports.UTP
{
/// <summary>Queue for batched messages received through UTP.</summary>
/// <remarks>This is meant as a companion to <see cref="BatchedSendQueue"/>.</remarks>
internal class BatchedReceiveQueue
{
private byte[] m_Data;
private int m_Offset;
private int m_Length;
public bool IsEmpty => m_Length <= 0;
/// <summary>
/// Construct a new receive queue from a <see cref="DataStreamReader"/> returned by
/// <see cref="NetworkDriver"/> when popping a data event.
/// </summary>
/// <param name="reader">The <see cref="DataStreamReader"/> to construct from.</param>
public BatchedReceiveQueue(DataStreamReader reader)
{
m_Data = new byte[reader.Length];
unsafe
{
fixed (byte* dataPtr = m_Data)
{
reader.ReadBytes(dataPtr, reader.Length);
}
}
m_Offset = 0;
m_Length = reader.Length;
}
/// <summary>
/// Push the entire data from a <see cref="DataStreamReader"/> (as returned by popping an
/// event from a <see cref="NetworkDriver">) to the queue.
/// </summary>
/// <param name="reader">The <see cref="DataStreamReader"/> to push the data of.</param>
public void PushReader(DataStreamReader reader)
{
// Resize the array and copy the existing data to the beginning if there's not enough
// room to copy the reader's data at the end of the existing data.
var available = m_Data.Length - (m_Offset + m_Length);
if (available < reader.Length)
{
if (m_Length > 0)
{
Array.Copy(m_Data, m_Offset, m_Data, 0, m_Length);
}
m_Offset = 0;
while (m_Data.Length - m_Length < reader.Length)
{
Array.Resize(ref m_Data, m_Data.Length * 2);
}
}
unsafe
{
fixed (byte* dataPtr = m_Data)
{
reader.ReadBytes(dataPtr + m_Offset + m_Length, reader.Length);
}
}
m_Length += reader.Length;
}
/// <summary>Pop the next full message in the queue.</summary>
/// <returns>The message, or the default value if no more full messages.</returns>
public ArraySegment<byte> PopMessage()
{
if (m_Length < sizeof(int))
{
return default;
}
var messageLength = BitConverter.ToInt32(m_Data, m_Offset);
if (m_Length - sizeof(int) < messageLength)
{
return default;
}
var data = new ArraySegment<byte>(m_Data, m_Offset + sizeof(int), messageLength);
m_Offset += sizeof(int) + messageLength;
m_Length -= sizeof(int) + messageLength;
return data;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c275febadb27c4d18b41218e3353b84b
guid: e9ead10b891184bd5b8f2650fd66a5b1
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,233 @@
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Networking.Transport;
namespace Unity.Netcode.Transports.UTP
{
/// <summary>Queue for batched messages meant to be sent through UTP.</summary>
/// <remarks>
/// Messages should be pushed on the queue with <see cref="PushMessage"/>. To send batched
/// messages, call <see cref="FillWriter"> with the <see cref="DataStreamWriter"/> obtained from
/// <see cref="NetworkDriver.BeginSend"/>. This will fill the writer with as many messages as
/// possible. If the send is successful, call <see cref="Consume"/> to remove the data from the
/// queue.
///
/// This is meant as a companion to <see cref="BatchedReceiveQueue"/>, which should be used to
/// read messages sent with this queue.
/// </remarks>
internal struct BatchedSendQueue : IDisposable
{
private NativeArray<byte> m_Data;
private NativeArray<int> m_HeadTailIndices;
/// <summary>Overhead that is added to each message in the queue.</summary>
public const int PerMessageOverhead = sizeof(int);
// Indices into m_HeadTailIndicies.
private const int k_HeadInternalIndex = 0;
private const int k_TailInternalIndex = 1;
/// <summary>Index of the first byte of the oldest data in the queue.</summary>
private int HeadIndex
{
get { return m_HeadTailIndices[k_HeadInternalIndex]; }
set { m_HeadTailIndices[k_HeadInternalIndex] = value; }
}
/// <summary>Index one past the last byte of the most recent data in the queue.</summary>
private int TailIndex
{
get { return m_HeadTailIndices[k_TailInternalIndex]; }
set { m_HeadTailIndices[k_TailInternalIndex] = value; }
}
public int Length => TailIndex - HeadIndex;
public bool IsEmpty => HeadIndex == TailIndex;
public bool IsCreated => m_Data.IsCreated;
/// <summary>Construct a new empty send queue.</summary>
/// <param name="capacity">Maximum capacity of the send queue.</param>
public BatchedSendQueue(int capacity)
{
m_Data = new NativeArray<byte>(capacity, Allocator.Persistent);
m_HeadTailIndices = new NativeArray<int>(2, Allocator.Persistent);
HeadIndex = 0;
TailIndex = 0;
}
public void Dispose()
{
if (IsCreated)
{
m_Data.Dispose();
m_HeadTailIndices.Dispose();
}
}
/// <summary>Append data at the tail of the queue. No safety checks.</summary>
private void AppendDataAtTail(ArraySegment<byte> data)
{
unsafe
{
var writer = new DataStreamWriter((byte*)m_Data.GetUnsafePtr() + TailIndex, m_Data.Length - TailIndex);
writer.WriteInt(data.Count);
fixed (byte* dataPtr = data.Array)
{
writer.WriteBytes(dataPtr + data.Offset, data.Count);
}
}
TailIndex += sizeof(int) + data.Count;
}
/// <summary>Append a new message to the queue.</summary>
/// <param name="message">Message to append to the queue.</param>
/// <returns>
/// Whether the message was appended successfully. The only way it can fail is if there's
/// no more room in the queue. On failure, nothing is written to the queue.
/// </returns>
public bool PushMessage(ArraySegment<byte> message)
{
if (!IsCreated)
{
return false;
}
// Check if there's enough room after the current tail index.
if (m_Data.Length - TailIndex >= sizeof(int) + message.Count)
{
AppendDataAtTail(message);
return true;
}
// Check if there would be enough room if we moved data at the beginning of m_Data.
if (m_Data.Length - TailIndex + HeadIndex >= sizeof(int) + message.Count)
{
// Move the data back at the beginning of m_Data.
unsafe
{
UnsafeUtility.MemMove(m_Data.GetUnsafePtr(), (byte*)m_Data.GetUnsafePtr() + HeadIndex, Length);
}
TailIndex = Length;
HeadIndex = 0;
AppendDataAtTail(message);
return true;
}
return false;
}
/// <summary>
/// Fill as much of a <see cref="DataStreamWriter"/> as possible with data from the head of
/// the queue. Only full messages (and their length) are written to the writer.
/// </summary>
/// <remarks>
/// This does NOT actually consume anything from the queue. That is, calling this method
/// does not reduce the length of the queue. Callers are expected to call
/// <see cref="Consume"/> with the value returned by this method afterwards if the data can
/// be safely removed from the queue (e.g. if it was sent successfully).
///
/// This method should not be used together with <see cref="FillWriterWithBytes"> since this
/// could lead to a corrupted queue.
/// </remarks>
/// <param name="writer">The <see cref="DataStreamWriter"/> to write to.</param>
/// <returns>How many bytes were written to the writer.</returns>
public int FillWriterWithMessages(ref DataStreamWriter writer)
{
if (!IsCreated || Length == 0)
{
return 0;
}
unsafe
{
var reader = new DataStreamReader((byte*)m_Data.GetUnsafePtr() + HeadIndex, Length);
var writerAvailable = writer.Capacity;
var readerOffset = 0;
while (readerOffset < Length)
{
reader.SeekSet(readerOffset);
var messageLength = reader.ReadInt();
if (writerAvailable < sizeof(int) + messageLength)
{
break;
}
else
{
writer.WriteInt(messageLength);
var messageOffset = HeadIndex + reader.GetBytesRead();
writer.WriteBytes((byte*)m_Data.GetUnsafePtr() + messageOffset, messageLength);
writerAvailable -= sizeof(int) + messageLength;
readerOffset += sizeof(int) + messageLength;
}
}
return writer.Capacity - writerAvailable;
}
}
/// <summary>
/// Fill the given <see cref="DataStreamWriter"/> with as many bytes from the queue as
/// possible, disregarding message boundaries.
/// </summary>
///<remarks>
/// This does NOT actually consume anything from the queue. That is, calling this method
/// does not reduce the length of the queue. Callers are expected to call
/// <see cref="Consume"/> with the value returned by this method afterwards if the data can
/// be safely removed from the queue (e.g. if it was sent successfully).
///
/// This method should not be used together with <see cref="FillWriterWithMessages"/> since
/// this could lead to reading messages from a corrupted queue.
/// </remarks>
/// <param name="writer">The <see cref="DataStreamWriter"/> to write to.</param>
/// <returns>How many bytes were written to the writer.</returns>
public int FillWriterWithBytes(ref DataStreamWriter writer)
{
if (!IsCreated || Length == 0)
{
return 0;
}
var copyLength = Math.Min(writer.Capacity, Length);
unsafe
{
writer.WriteBytes((byte*)m_Data.GetUnsafePtr() + HeadIndex, copyLength);
}
return copyLength;
}
/// <summary>Consume a number of bytes from the head of the queue.</summary>
/// <remarks>
/// This should only be called with a size that matches the last value returned by
/// <see cref="FillWriter"/>. Anything else will result in a corrupted queue.
/// </remarks>
/// <param name="size">Number of bytes to consume from the queue.</param>
public void Consume(int size)
{
if (size >= Length)
{
HeadIndex = 0;
TailIndex = 0;
}
else
{
HeadIndex += size;
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: bd9e1475e8c8e4a6d935fe2409e3bd26
guid: ddf8f97f695d740f297dc42242b76b8c
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,8 @@
namespace Unity.Netcode.Transports.UTP
{
public struct NetworkMetricsContext
{
public uint PacketSentCount;
public uint PacketReceivedCount;
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5cf75026c2ab86646aac16b39d7259ad
guid: adb0270501ff1421896ce15cc75bd56a
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,70 @@
#if MULTIPLAYER_TOOLS
#if MULTIPLAYER_TOOLS_1_0_0_PRE_7
using AOT;
using Unity.Burst;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Networking.Transport;
using UnityEngine;
namespace Unity.Netcode.Transports.UTP
{
[BurstCompile]
internal unsafe struct NetworkMetricsPipelineStage : INetworkPipelineStage
{
static TransportFunctionPointer<NetworkPipelineStage.ReceiveDelegate> ReceiveFunction = new TransportFunctionPointer<NetworkPipelineStage.ReceiveDelegate>(Receive);
static TransportFunctionPointer<NetworkPipelineStage.SendDelegate> SendFunction = new TransportFunctionPointer<NetworkPipelineStage.SendDelegate>(Send);
static TransportFunctionPointer<NetworkPipelineStage.InitializeConnectionDelegate> InitializeConnectionFunction = new TransportFunctionPointer<NetworkPipelineStage.InitializeConnectionDelegate>(InitializeConnection);
public NetworkPipelineStage StaticInitialize(byte* staticInstanceBuffer,
int staticInstanceBufferLength,
NetworkSettings settings)
{
return new NetworkPipelineStage(
ReceiveFunction,
SendFunction,
InitializeConnectionFunction,
ReceiveCapacity: 0,
SendCapacity: 0,
HeaderCapacity: 0,
SharedStateCapacity: UnsafeUtility.SizeOf<NetworkMetricsContext>());
}
public int StaticSize => 0;
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(NetworkPipelineStage.ReceiveDelegate))]
private static void Receive(ref NetworkPipelineContext networkPipelineContext,
ref InboundRecvBuffer inboundReceiveBuffer,
ref NetworkPipelineStage.Requests requests,
int systemHeaderSize)
{
var networkMetricContext = (NetworkMetricsContext*)networkPipelineContext.internalSharedProcessBuffer;
networkMetricContext->PacketReceivedCount++;
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(NetworkPipelineStage.SendDelegate))]
private static int Send(ref NetworkPipelineContext networkPipelineContext,
ref InboundSendBuffer inboundSendBuffer,
ref NetworkPipelineStage.Requests requests,
int systemHeaderSize)
{
var networkMetricContext = (NetworkMetricsContext*)networkPipelineContext.internalSharedProcessBuffer;
networkMetricContext->PacketSentCount++;
return 0;
}
[BurstCompile(DisableDirectCall = true)]
[MonoPInvokeCallback(typeof(NetworkPipelineStage.InitializeConnectionDelegate))]
private static void InitializeConnection(byte* staticInstanceBuffer, int staticInstanceBufferLength,
byte* sendProcessBuffer, int sendProcessBufferLength, byte* receiveProcessBuffer, int receiveProcessBufferLength,
byte* sharedProcessBuffer, int sharedProcessBufferLength)
{
var networkMetricContext = (NetworkMetricsContext*)sharedProcessBuffer;
networkMetricContext->PacketSentCount = 0;
networkMetricContext->PacketReceivedCount = 0;
}
}
}
#endif
#endif

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 69c3c1c5a885d4aed99ee2e1fa40f763
guid: 52b1ce9f83ce049c59327064bf70cee8
MonoImporter:
externalObjects: {}
serializedVersion: 2

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6960e84d07fb87f47956e7a81d71c4e6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -10,7 +10,9 @@
"Unity.Multiplayer.Tools.NetStats",
"Unity.Multiplayer.Tools.NetStatsReporting",
"Unity.Multiplayer.Tools.NetworkSolutionInterface",
"Unity.Collections"
"Unity.Networking.Transport",
"Unity.Collections",
"Unity.Burst"
],
"allowUnsafeCode": true,
"versionDefines": [
@@ -26,8 +28,8 @@
},
{
"name": "com.unity.multiplayer.tools",
"expression": "1.0.0-pre.4",
"define": "MULTIPLAYER_TOOLS_1_0_0_PRE_4"
"expression": "1.0.0-pre.7",
"define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7"
}
]
}
}