diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7213ea9..3423d9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
+## [2.1.1] - 2024-10-18
+
+### Added
+
+- Added ability to edit the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` within the inspector view. (#3097)
+- Added `IContactEventHandlerWithInfo` that derives from `IContactEventHandler` that can be updated per frame to provide `ContactEventHandlerInfo` information to the `RigidbodyContactEventManager` when processing collisions. (#3094)
+ - `ContactEventHandlerInfo.ProvideNonRigidBodyContactEvents`: When set to true, non-`Rigidbody` collisions with the registered `Rigidbody` will generate contact event notifications. (#3094)
+ - `ContactEventHandlerInfo.HasContactEventPriority`: When set to true, the `Rigidbody` will be prioritized as the instance that generates the event if the `Rigidbody` colliding does not have priority. (#3094)
+- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3088)
+- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3088)
+
+### Fixed
+
+- Fixed issue where `NetworkPrefabProcessor` would not mark the prefab list as dirty and prevent saving the `DefaultNetworkPrefabs` asset when only imports or only deletes were detected.(#3103)
+- Fixed an issue where nested `NetworkTransform` components in owner authoritative mode cleared their initial settings on the server, causing improper synchronization. (#3099)
+- Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096)
+- Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092)
+- Fixed an issue where newly synchronizing clients would always receive current `NetworkVariable` values, potentially causing issues with collections if there were pending updates. Now, pending state updates serialize previous values to avoid duplicates on new clients. (#3081)
+- Fixed issue where changing ownership would mark every `NetworkVariable` dirty. Now, it will only mark any `NetworkVariable` with owner read permissions as dirty and will send/flush any pending updates to all clients prior to sending the change in ownership message. (#3081)
+- Fixed an issue where transferring ownership of `NetworkVariable` collections didn't update the new owner’s previous value, causing the last added value to be detected as a change during additions or removals. (#3081)
+- Fixed issue where a client (or server) with no write permissions for a `NetworkVariable` using a standard .NET collection type could still modify the collection which could cause various issues depending upon the modification and collection type. (#3081)
+- Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078)
+- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077)
+- Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075)
+
+### Changed
+
+- Changed `NetworkConfig.AutoSpawnPlayerPrefabClientSide` is no longer automatically set when starting `NetworkManager`. (#3097)
+- Updated `NetworkVariableDeltaMessage` so the server now forwards delta state updates from clients immediately, instead of waiting until the end of the frame or the next network tick. (#3081)
+
## [2.0.0] - 2024-09-12
### Added
diff --git a/Editor/Configuration/NetworkPrefabProcessor.cs b/Editor/Configuration/NetworkPrefabProcessor.cs
index 879a8c3..55f5fcb 100644
--- a/Editor/Configuration/NetworkPrefabProcessor.cs
+++ b/Editor/Configuration/NetworkPrefabProcessor.cs
@@ -132,7 +132,7 @@ namespace Unity.Netcode.Editor.Configuration
// Process the imported and deleted assets
var markDirty = ProcessImportedAssets(importedAssets);
- markDirty &= ProcessDeletedAssets(deletedAssets);
+ markDirty |= ProcessDeletedAssets(deletedAssets);
if (markDirty)
{
diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs
index 5445b0d..0684d74 100644
--- a/Editor/NetworkManagerEditor.cs
+++ b/Editor/NetworkManagerEditor.cs
@@ -31,6 +31,7 @@ namespace Unity.Netcode.Editor
private SerializedProperty m_NetworkTransportProperty;
private SerializedProperty m_TickRateProperty;
#if MULTIPLAYER_SERVICES_SDK_INSTALLED
+ private SerializedProperty m_AutoSpawnPlayerPrefabClientSide;
private SerializedProperty m_NetworkTopologyProperty;
#endif
private SerializedProperty m_ClientConnectionBufferTimeoutProperty;
@@ -104,6 +105,11 @@ namespace Unity.Netcode.Editor
m_TickRateProperty = m_NetworkConfigProperty.FindPropertyRelative("TickRate");
#if MULTIPLAYER_SERVICES_SDK_INSTALLED
m_NetworkTopologyProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkTopology");
+ // Only display the auto spawn property when the distributed authority network topology is selected
+ if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority)
+ {
+ m_AutoSpawnPlayerPrefabClientSide = m_NetworkConfigProperty.FindPropertyRelative("AutoSpawnPlayerPrefabClientSide");
+ }
#endif
m_ClientConnectionBufferTimeoutProperty = m_NetworkConfigProperty.FindPropertyRelative("ClientConnectionBufferTimeout");
m_ConnectionApprovalProperty = m_NetworkConfigProperty.FindPropertyRelative("ConnectionApproval");
@@ -120,8 +126,6 @@ namespace Unity.Netcode.Editor
#if MULTIPLAYER_TOOLS
m_NetworkMessageMetrics = m_NetworkConfigProperty.FindPropertyRelative("NetworkMessageMetrics");
#endif
-
-
m_RpcHashSizeProperty = m_NetworkConfigProperty.FindPropertyRelative("RpcHashSize");
m_PrefabsList = m_NetworkConfigProperty
.FindPropertyRelative(nameof(NetworkConfig.Prefabs))
@@ -144,6 +148,11 @@ namespace Unity.Netcode.Editor
m_TickRateProperty = m_NetworkConfigProperty.FindPropertyRelative("TickRate");
#if MULTIPLAYER_SERVICES_SDK_INSTALLED
m_NetworkTopologyProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkTopology");
+ // Only display the auto spawn property when the distributed authority network topology is selected
+ if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority)
+ {
+ m_AutoSpawnPlayerPrefabClientSide = m_NetworkConfigProperty.FindPropertyRelative("AutoSpawnPlayerPrefabClientSide");
+ }
#endif
m_ClientConnectionBufferTimeoutProperty = m_NetworkConfigProperty.FindPropertyRelative("ClientConnectionBufferTimeout");
m_ConnectionApprovalProperty = m_NetworkConfigProperty.FindPropertyRelative("ConnectionApproval");
@@ -173,10 +182,11 @@ namespace Unity.Netcode.Editor
if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient)
{
serializedObject.Update();
+
EditorGUILayout.PropertyField(m_RunInBackgroundProperty);
EditorGUILayout.PropertyField(m_LogLevelProperty);
-
EditorGUILayout.Space();
+
EditorGUILayout.LabelField("Network Settings", EditorStyles.boldLabel);
#if MULTIPLAYER_SERVICES_SDK_INSTALLED
EditorGUILayout.PropertyField(m_NetworkTopologyProperty);
@@ -222,8 +232,17 @@ namespace Unity.Netcode.Editor
EditorGUILayout.Space();
EditorGUILayout.LabelField("Prefab Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_ForceSamePrefabsProperty);
+#if MULTIPLAYER_SERVICES_SDK_INSTALLED
+ // Only display the auto spawn property when the distributed authority network topology is selected
+ if (m_NetworkManager.NetworkConfig.NetworkTopology == NetworkTopologyTypes.DistributedAuthority)
+ {
+ EditorGUILayout.PropertyField(m_AutoSpawnPlayerPrefabClientSide, new GUIContent("Auto Spawn Player Prefab"));
+ }
+#endif
EditorGUILayout.PropertyField(m_PlayerPrefabProperty, new GUIContent("Default Player Prefab"));
+
+
if (m_NetworkManager.NetworkConfig.HasOldPrefabList())
{
EditorGUILayout.HelpBox("Network Prefabs serialized in old format. Migrate to new format to edit the list.", MessageType.Info);
diff --git a/Runtime/Components/NetworkTransform.cs b/Runtime/Components/NetworkTransform.cs
index b9d8eab..a103b0f 100644
--- a/Runtime/Components/NetworkTransform.cs
+++ b/Runtime/Components/NetworkTransform.cs
@@ -1463,8 +1463,6 @@ namespace Unity.Netcode.Components
// For test logging purposes
internal NetworkTransformState SynchronizeState;
- // DANGO-TODO: We will want to remove this when we migrate NetworkTransforms to a dedicated internal message
- private const ushort k_NetworkTransformStateMagic = 0xf48d;
#endregion
#region ONSYNCHRONIZE
@@ -1489,19 +1487,10 @@ namespace Unity.Netcode.Components
HalfVectorRotation = new HalfVector4(),
HalfVectorScale = new HalfVector3(),
NetworkDeltaPosition = new NetworkDeltaPosition(),
-
};
if (serializer.IsWriter)
{
- // DANGO-TODO: This magic value is sent to the server in order to identify the network transform.
- // The server discards it before forwarding synchronization data to other clients.
- if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection)
- {
- var writer = serializer.GetFastBufferWriter();
- writer.WriteValueSafe(k_NetworkTransformStateMagic);
- }
-
SynchronizeState.IsTeleportingNextFrame = true;
var transformToCommit = transform;
// If we are using Half Float Precision, then we want to only synchronize the authority's m_HalfPositionState.FullPosition in order for
@@ -3060,12 +3049,44 @@ namespace Unity.Netcode.Components
base.InternalOnNetworkSessionSynchronized();
}
+ private void ApplyPlayerTransformState()
+ {
+ SynchronizeState.InLocalSpace = InLocalSpace;
+ SynchronizeState.UseInterpolation = Interpolate;
+ SynchronizeState.QuaternionSync = UseQuaternionSynchronization;
+ SynchronizeState.UseHalfFloatPrecision = UseHalfFloatPrecision;
+ SynchronizeState.QuaternionCompression = UseQuaternionCompression;
+ SynchronizeState.UsePositionSlerp = SlerpPosition;
+ }
+
///
/// For dynamically spawned NetworkObjects, when the non-authority instance's client is already connected and
/// the SynchronizeState is still pending synchronization then we want to finalize the synchornization at this time.
///
protected internal override void InternalOnNetworkPostSpawn()
{
+ // This is a special case for client-server where a server is spawning an owner authoritative NetworkObject but has yet to serialize anything.
+ // When the server detects that:
+ // - We are not in a distributed authority session (DAHost check).
+ // - This is the first/root NetworkTransform.
+ // - We are in owner authoritative mode.
+ // - The NetworkObject is not owned by the server.
+ // - The SynchronizeState.IsSynchronizing is set to false.
+ // Then we want to:
+ // - Force the "IsSynchronizing" flag so the NetworkTransform has its state updated properly and runs through the initialization again.
+ // - Make sure the SynchronizingState is updated to the instantiated prefab's default flags/settings.
+ if (NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode && m_IsFirstNetworkTransform && !OnIsServerAuthoritative() && !IsOwner && !SynchronizeState.IsSynchronizing)
+ {
+ // Assure the first/root NetworkTransform has the synchronizing flag set so the server runs through the final post initialization steps
+ SynchronizeState.IsSynchronizing = true;
+ // Assure the SynchronizeState matches the initial prefab's values for each associated NetworkTransfrom (this includes root + all children)
+ foreach (var child in NetworkObject.NetworkTransforms)
+ {
+ child.ApplyPlayerTransformState();
+ }
+ // Now fall through to the final synchronization portion of the spawning for NetworkTransform
+ }
+
if (!CanCommitToTransform && NetworkManager.IsConnectedClient && SynchronizeState.IsSynchronizing)
{
NonAuthorityFinalizeSynchronization();
diff --git a/Runtime/Components/RigidbodyContactEventManager.cs b/Runtime/Components/RigidbodyContactEventManager.cs
index 9471a94..d0808d2 100644
--- a/Runtime/Components/RigidbodyContactEventManager.cs
+++ b/Runtime/Components/RigidbodyContactEventManager.cs
@@ -6,13 +6,70 @@ using UnityEngine;
namespace Unity.Netcode.Components
{
+ ///
+ /// Information a returns to via
+ /// if the registers itself with as opposed to .
+ ///
+ public struct ContactEventHandlerInfo
+ {
+ ///
+ /// When set to true, the will include non-Rigidbody based contact events.
+ /// When the invokes the it will return null in place
+ /// of the collidingBody parameter if the contact event occurred with a collider that is not registered with the .
+ ///
+ public bool ProvideNonRigidBodyContactEvents;
+ ///
+ /// When set to true, the will prioritize invoking
+ /// if it is the 2nd colliding body in the contact pair being processed. With distributed authority, setting this value to true when a is owned by the local client
+ /// will assure is only invoked on the authoritative side.
+ ///
+ public bool HasContactEventPriority;
+ }
+
+ ///
+ /// Default implementation required to register a with a instance.
+ ///
+ ///
+ /// Recommended to implement this method on a component
+ ///
public interface IContactEventHandler
{
+ ///
+ /// Should return a .
+ ///
Rigidbody GetRigidbody();
+ ///
+ /// Invoked by the instance.
+ ///
+ /// A unique contact event identifier.
+ /// The average normal of the collision between two colliders.
+ /// If not null, this will be a registered that was part of the collision contact event.
+ /// The world space location of the contact event.
+ /// Will be set if this is a collision stay contact event (i.e. it is not the first contact event and continually has contact)
+ /// The average normal of the collision stay contact over time.
void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default);
}
+ ///
+ /// This is an extended version of and can be used to register a with a instance.
+ /// This provides additional information to the for each set of contact events it is processing.
+ ///
+ public interface IContactEventHandlerWithInfo : IContactEventHandler
+ {
+ ///
+ /// Invoked by for each set of contact events it is processing (prior to processing).
+ ///
+ ///
+ ContactEventHandlerInfo GetContactEventHandlerInfo();
+ }
+
+ ///
+ /// Add this component to an in-scene placed GameObject to provide faster collision event processing between instances and optionally static colliders.
+ ///
+ ///
+ ///
+ ///
[AddComponentMenu("Netcode/Rigidbody Contact Event Manager")]
public class RigidbodyContactEventManager : MonoBehaviour
{
@@ -34,6 +91,7 @@ namespace Unity.Netcode.Components
private readonly Dictionary m_RigidbodyMapping = new Dictionary();
private readonly Dictionary m_HandlerMapping = new Dictionary();
+ private readonly Dictionary m_HandlerInfo = new Dictionary();
private void OnEnable()
{
@@ -49,6 +107,15 @@ namespace Unity.Netcode.Components
Instance = this;
}
+ ///
+ /// Any implementation can register a to be handled by this instance.
+ ///
+ ///
+ /// You should enable for each associated with the being registered.
+ /// You can enable this during run time or within the editor's inspector view.
+ ///
+ /// or
+ /// true to register and false to remove from being registered
public void RegisterHandler(IContactEventHandler contactEventHandler, bool register = true)
{
var rigidbody = contactEventHandler.GetRigidbody();
@@ -64,6 +131,22 @@ namespace Unity.Netcode.Components
{
m_HandlerMapping.Add(instanceId, contactEventHandler);
}
+
+ if (!m_HandlerInfo.ContainsKey(instanceId))
+ {
+ var handlerInfo = new ContactEventHandlerInfo()
+ {
+ HasContactEventPriority = true,
+ ProvideNonRigidBodyContactEvents = false,
+ };
+ var handlerWithInfo = contactEventHandler as IContactEventHandlerWithInfo;
+
+ if (handlerWithInfo != null)
+ {
+ handlerInfo = handlerWithInfo.GetContactEventHandlerInfo();
+ }
+ m_HandlerInfo.Add(instanceId, handlerInfo);
+ }
}
else
{
@@ -88,25 +171,98 @@ namespace Unity.Netcode.Components
private void ProcessCollisions()
{
+ foreach (var contactEventHandler in m_HandlerMapping)
+ {
+ var handlerWithInfo = contactEventHandler.Value as IContactEventHandlerWithInfo;
+
+ if (handlerWithInfo != null)
+ {
+ m_HandlerInfo[contactEventHandler.Key] = handlerWithInfo.GetContactEventHandlerInfo();
+ }
+ else
+ {
+ var info = m_HandlerInfo[contactEventHandler.Key];
+ info.HasContactEventPriority = !m_RigidbodyMapping[contactEventHandler.Key].isKinematic;
+ m_HandlerInfo[contactEventHandler.Key] = info;
+ }
+ }
+
+ ContactEventHandlerInfo contactEventHandlerInfo0;
+ ContactEventHandlerInfo contactEventHandlerInfo1;
+
// Process all collisions
for (int i = 0; i < m_Count; i++)
{
var thisInstanceID = m_ResultsArray[i].ThisInstanceID;
var otherInstanceID = m_ResultsArray[i].OtherInstanceID;
- var rb0Valid = thisInstanceID != 0 && m_RigidbodyMapping.ContainsKey(thisInstanceID);
- var rb1Valid = otherInstanceID != 0 && m_RigidbodyMapping.ContainsKey(otherInstanceID);
- // Only notify registered rigid bodies.
- if (!rb0Valid || !rb1Valid || !m_HandlerMapping.ContainsKey(thisInstanceID))
+ var contactHandler0 = (IContactEventHandler)null;
+ var contactHandler1 = (IContactEventHandler)null;
+ var preferredContactHandler = (IContactEventHandler)null;
+ var preferredContactHandlerNonRigidbody = false;
+ var preferredRigidbody = (Rigidbody)null;
+ var otherContactHandler = (IContactEventHandler)null;
+ var otherRigidbody = (Rigidbody)null;
+
+ var otherContactHandlerNonRigidbody = false;
+
+ if (m_RigidbodyMapping.ContainsKey(thisInstanceID))
+ {
+ contactHandler0 = m_HandlerMapping[thisInstanceID];
+ contactEventHandlerInfo0 = m_HandlerInfo[thisInstanceID];
+ if (contactEventHandlerInfo0.HasContactEventPriority)
+ {
+ preferredContactHandler = contactHandler0;
+ preferredContactHandlerNonRigidbody = contactEventHandlerInfo0.ProvideNonRigidBodyContactEvents;
+ preferredRigidbody = m_RigidbodyMapping[thisInstanceID];
+ }
+ else
+ {
+ otherContactHandler = contactHandler0;
+ otherContactHandlerNonRigidbody = contactEventHandlerInfo0.ProvideNonRigidBodyContactEvents;
+ otherRigidbody = m_RigidbodyMapping[thisInstanceID];
+ }
+ }
+
+ if (m_RigidbodyMapping.ContainsKey(otherInstanceID))
+ {
+ contactHandler1 = m_HandlerMapping[otherInstanceID];
+ contactEventHandlerInfo1 = m_HandlerInfo[otherInstanceID];
+ if (contactEventHandlerInfo1.HasContactEventPriority && preferredContactHandler == null)
+ {
+ preferredContactHandler = contactHandler1;
+ preferredContactHandlerNonRigidbody = contactEventHandlerInfo1.ProvideNonRigidBodyContactEvents;
+ preferredRigidbody = m_RigidbodyMapping[otherInstanceID];
+ }
+ else
+ {
+ otherContactHandler = contactHandler1;
+ otherContactHandlerNonRigidbody = contactEventHandlerInfo1.ProvideNonRigidBodyContactEvents;
+ otherRigidbody = m_RigidbodyMapping[otherInstanceID];
+ }
+ }
+
+ if (preferredContactHandler == null && otherContactHandler != null)
+ {
+ preferredContactHandler = otherContactHandler;
+ preferredContactHandlerNonRigidbody = otherContactHandlerNonRigidbody;
+ preferredRigidbody = otherRigidbody;
+ otherContactHandler = null;
+ otherContactHandlerNonRigidbody = false;
+ otherRigidbody = null;
+ }
+
+ if (preferredContactHandler == null || (preferredContactHandler != null && otherContactHandler == null && !preferredContactHandlerNonRigidbody))
{
continue;
}
+
if (m_ResultsArray[i].HasCollisionStay)
{
- m_HandlerMapping[thisInstanceID].ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, m_RigidbodyMapping[otherInstanceID], m_ResultsArray[i].ContactPoint, m_ResultsArray[i].HasCollisionStay, m_ResultsArray[i].AverageCollisionStayNormal);
+ preferredContactHandler.ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, otherRigidbody, m_ResultsArray[i].ContactPoint, m_ResultsArray[i].HasCollisionStay, m_ResultsArray[i].AverageCollisionStayNormal);
}
else
{
- m_HandlerMapping[thisInstanceID].ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, m_RigidbodyMapping[otherInstanceID], m_ResultsArray[i].ContactPoint);
+ preferredContactHandler.ContactEvent(m_EventId, m_ResultsArray[i].AverageNormal, otherRigidbody, m_ResultsArray[i].ContactPoint);
}
}
}
diff --git a/Runtime/Connection/NetworkConnectionManager.cs b/Runtime/Connection/NetworkConnectionManager.cs
index b5fff54..5c26df4 100644
--- a/Runtime/Connection/NetworkConnectionManager.cs
+++ b/Runtime/Connection/NetworkConnectionManager.cs
@@ -105,8 +105,12 @@ namespace Unity.Netcode
continue;
}
- peerClientIds[idx] = peerId;
- ++idx;
+ // This assures if the server has not timed out prior to the client synchronizing that it doesn't exceed the allocated peer count.
+ if (peerClientIds.Length > idx)
+ {
+ peerClientIds[idx] = peerId;
+ ++idx;
+ }
}
try
@@ -496,24 +500,32 @@ namespace Unity.Netcode
// Process the incoming message queue so that we get everything from the server disconnecting us or, if we are the server, so we got everything from that client.
MessageManager.ProcessIncomingMessageQueue();
- InvokeOnClientDisconnectCallback(clientId);
-
- if (LocalClient.IsHost)
- {
- InvokeOnPeerDisconnectedCallback(clientId);
- }
-
if (LocalClient.IsServer)
{
+ // We need to process the disconnection before notifying
OnClientDisconnectFromServer(clientId);
+
+ // Now notify the client has disconnected
+ InvokeOnClientDisconnectCallback(clientId);
+
+ if (LocalClient.IsHost)
+ {
+ InvokeOnPeerDisconnectedCallback(clientId);
+ }
}
- else // As long as we are not in the middle of a shutdown
- if (!NetworkManager.ShutdownInProgress)
+ else
{
- // We must pass true here and not process any sends messages as we are no longer connected.
- // Otherwise, attempting to process messages here can cause an exception within UnityTransport
- // as the client ID is no longer valid.
- NetworkManager.Shutdown(true);
+ // Notify local client of disconnection
+ InvokeOnClientDisconnectCallback(clientId);
+
+ // As long as we are not in the middle of a shutdown
+ if (!NetworkManager.ShutdownInProgress)
+ {
+ // We must pass true here and not process any sends messages as we are no longer connected.
+ // Otherwise, attempting to process messages here can cause an exception within UnityTransport
+ // as the client ID is no longer valid.
+ NetworkManager.Shutdown(true);
+ }
}
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportDisconnect.End();
@@ -552,9 +564,6 @@ namespace Unity.Netcode
var message = new ConnectionRequestMessage
{
CMBServiceConnection = NetworkManager.CMBServiceConnection,
- TickRate = NetworkManager.NetworkConfig.TickRate,
- EnableSceneManagement = NetworkManager.NetworkConfig.EnableSceneManagement,
-
// Since only a remote client will send a connection request, we should always force the rebuilding of the NetworkConfig hash value
ConfigHash = NetworkManager.NetworkConfig.GetConfig(false),
ShouldSendConnectionData = NetworkManager.NetworkConfig.ConnectionApproval,
@@ -562,6 +571,12 @@ namespace Unity.Netcode
MessageVersions = new NativeArray(MessageManager.MessageHandlers.Length, Allocator.Temp)
};
+ if (NetworkManager.CMBServiceConnection)
+ {
+ message.ClientConfig.TickRate = NetworkManager.NetworkConfig.TickRate;
+ message.ClientConfig.EnableSceneManagement = NetworkManager.NetworkConfig.EnableSceneManagement;
+ }
+
for (int index = 0; index < MessageManager.MessageHandlers.Length; index++)
{
if (MessageManager.MessageTypes[index] != null)
@@ -739,8 +754,8 @@ namespace Unity.Netcode
// Server-side spawning (only if there is a prefab hash or player prefab provided)
if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null))
{
- var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault())
- : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position.GetValueOrDefault(), response.Rotation.GetValueOrDefault());
+ var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position ?? null, response.Rotation ?? null)
+ : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position ?? null, response.Rotation ?? null);
// Spawn the player NetworkObject locally
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(
@@ -884,7 +899,7 @@ namespace Unity.Netcode
///
/// Client-Side Spawning in distributed authority mode uses this to spawn the player.
///
- internal void CreateAndSpawnPlayer(ulong ownerId, Vector3 position = default, Quaternion rotation = default)
+ internal void CreateAndSpawnPlayer(ulong ownerId)
{
if (NetworkManager.DistributedAuthorityMode && NetworkManager.AutoSpawnPlayerPrefabClientSide)
{
@@ -892,7 +907,7 @@ namespace Unity.Netcode
if (playerPrefab != null)
{
var globalObjectIdHash = playerPrefab.GetComponent().GlobalObjectIdHash;
- var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, position, rotation);
+ var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation);
networkObject.IsSceneObject = false;
networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene);
}
diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs
index b5d5495..bcdd25f 100644
--- a/Runtime/Core/NetworkBehaviour.cs
+++ b/Runtime/Core/NetworkBehaviour.cs
@@ -826,6 +826,13 @@ namespace Unity.Netcode
internal void InternalOnGainedOwnership()
{
UpdateNetworkProperties();
+ // New owners need to assure any NetworkVariables they have write permissions
+ // to are updated so the previous and original values are aligned with the
+ // current value (primarily for collections).
+ if (OwnerClientId == NetworkManager.LocalClientId)
+ {
+ UpdateNetworkVariableOnOwnershipChanged();
+ }
OnGainedOwnership();
}
@@ -1016,9 +1023,14 @@ namespace Unity.Netcode
internal readonly List NetworkVariableIndexesToReset = new List();
internal readonly HashSet NetworkVariableIndexesToResetSet = new HashSet();
- internal void NetworkVariableUpdate(ulong targetClientId)
+ ///
+ /// Determines if a NetworkVariable should have any changes to state sent out
+ ///
+ /// target to send the updates to
+ /// specific to change in ownership
+ internal void NetworkVariableUpdate(ulong targetClientId, bool forceSend = false)
{
- if (!CouldHaveDirtyNetworkVariables())
+ if (!forceSend && !CouldHaveDirtyNetworkVariables())
{
return;
}
@@ -1069,7 +1081,11 @@ namespace Unity.Netcode
NetworkBehaviourIndex = behaviourIndex,
NetworkBehaviour = this,
TargetClientId = targetClientId,
- DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j]
+ DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j],
+ // By sending the network delivery we can forward messages immediately as opposed to processing them
+ // at the end. While this will send updates to clients that cannot read, the handler will ignore anything
+ // sent to a client that does not have read permissions.
+ NetworkDelivery = m_DeliveryTypesForNetworkVariableGroups[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,
@@ -1114,6 +1130,26 @@ namespace Unity.Netcode
return false;
}
+ ///
+ /// Invoked on a new client to assure the previous and original values
+ /// are synchronized with the current known value.
+ ///
+ ///
+ /// Primarily for collections to assure the previous value(s) is/are the
+ /// same as the current value(s) in order to not re-send already known entries.
+ ///
+ internal void UpdateNetworkVariableOnOwnershipChanged()
+ {
+ for (int j = 0; j < NetworkVariableFields.Count; j++)
+ {
+ // Only invoke OnInitialize on NetworkVariables the owner can write to
+ if (NetworkVariableFields[j].CanClientWrite(OwnerClientId))
+ {
+ NetworkVariableFields[j].OnInitialize();
+ }
+ }
+ }
+
internal void MarkVariablesDirty(bool dirty)
{
for (int j = 0; j < NetworkVariableFields.Count; j++)
@@ -1122,6 +1158,17 @@ namespace Unity.Netcode
}
}
+ internal void MarkOwnerReadVariablesDirty()
+ {
+ for (int j = 0; j < NetworkVariableFields.Count; j++)
+ {
+ if (NetworkVariableFields[j].ReadPerm == NetworkVariableReadPermission.Owner)
+ {
+ NetworkVariableFields[j].SetDirty(true);
+ }
+ }
+ }
+
///
/// Synchronizes by setting only the NetworkVariable field values that the client has permission to read.
/// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject)
@@ -1172,17 +1219,24 @@ namespace Unity.Netcode
// The way we do packing, any value > 63 in a ushort will use the full 2 bytes to represent.
writer.WriteValueSafe((ushort)0);
var startPos = writer.Position;
- NetworkVariableFields[j].WriteField(writer);
+ // Write the NetworkVariable field value
+ // WriteFieldSynchronization will write the current value only if there are no pending changes.
+ // Otherwise, it will write the previous value if there are pending changes since the pending
+ // changes will be sent shortly after the client's synchronization.
+ NetworkVariableFields[j].WriteFieldSynchronization(writer);
var size = writer.Position - startPos;
writer.Seek(writePos);
- // Write the NetworkVariable value
+ // Write the NetworkVariable field value size
writer.WriteValueSafe((ushort)size);
writer.Seek(startPos + size);
}
else // Client-Server Only: Should only ever be invoked when using a client-server NetworkTopology
{
- // Write the NetworkVariable value
- NetworkVariableFields[j].WriteField(writer);
+ // Write the NetworkVariable field value
+ // WriteFieldSynchronization will write the current value only if there are no pending changes.
+ // Otherwise, it will write the previous value if there are pending changes since the pending
+ // changes will be sent shortly after the client's synchronization.
+ NetworkVariableFields[j].WriteFieldSynchronization(writer);
}
}
else if (ensureLengthSafety)
diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs
index 7bf2030..9062ebf 100644
--- a/Runtime/Core/NetworkBehaviourUpdater.cs
+++ b/Runtime/Core/NetworkBehaviourUpdater.cs
@@ -19,10 +19,15 @@ namespace Unity.Netcode
internal void AddForUpdate(NetworkObject networkObject)
{
+ // Since this is a HashSet, we don't need to worry about duplicate entries
m_PendingDirtyNetworkObjects.Add(networkObject);
}
- internal void NetworkBehaviourUpdate()
+ ///
+ /// Sends NetworkVariable deltas
+ ///
+ /// internal only, when changing ownership we want to send this before the change in ownership message
+ internal void NetworkBehaviourUpdate(bool forceSend = false)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
m_NetworkBehaviourUpdate.Begin();
@@ -53,7 +58,7 @@ namespace Unity.Netcode
// Sync just the variables for just the objects this client sees
for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++)
{
- dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId);
+ dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId, forceSend);
}
}
}
@@ -72,7 +77,7 @@ namespace Unity.Netcode
}
for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++)
{
- sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId);
+ sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId, forceSend);
}
}
}
@@ -85,19 +90,24 @@ namespace Unity.Netcode
var behaviour = dirtyObj.ChildNetworkBehaviours[k];
for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++)
{
+ // Set to true for NetworkVariable to ignore duplication of the
+ // "internal original value" for collections support.
+ behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = true;
if (behaviour.NetworkVariableFields[i].IsDirty() &&
!behaviour.NetworkVariableIndexesToResetSet.Contains(i))
{
behaviour.NetworkVariableIndexesToResetSet.Add(i);
behaviour.NetworkVariableIndexesToReset.Add(i);
}
+ // Reset back to false when done
+ behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = false;
}
}
}
// Now, reset all the no-longer-dirty variables
foreach (var dirtyobj in m_DirtyNetworkObjects)
{
- dirtyobj.PostNetworkVariableWrite();
+ dirtyobj.PostNetworkVariableWrite(forceSend);
// Once done processing, we set the previous owner id to the current owner id
dirtyobj.PreviousOwnerId = dirtyobj.OwnerClientId;
}
diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs
index 0cdb995..de08ae0 100644
--- a/Runtime/Core/NetworkManager.cs
+++ b/Runtime/Core/NetworkManager.cs
@@ -5,6 +5,7 @@ using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
+using PackageInfo = UnityEditor.PackageManager.PackageInfo;
#endif
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;
@@ -17,6 +18,17 @@ namespace Unity.Netcode
[AddComponentMenu("Netcode/Network Manager", -100)]
public class NetworkManager : MonoBehaviour, INetworkUpdateSystem
{
+ ///
+ /// Subscribe to this static event to get notifications when a instance has been instantiated.
+ ///
+ public static event Action OnInstantiated;
+
+ ///
+ /// Subscribe to this static event to get notifications when a instance is being destroyed.
+ ///
+ public static event Action OnDestroying;
+
+
#if UNITY_EDITOR
// Inspector view expand/collapse settings for this derived child class
[HideInInspector]
@@ -874,6 +886,7 @@ namespace Unity.Netcode
internal Override PortOverride;
+
#if UNITY_EDITOR
internal static INetworkManagerHelper NetworkManagerHelper;
@@ -900,6 +913,11 @@ namespace Unity.Netcode
}
+ private PackageInfo GetPackageInfo(string packageName)
+ {
+ return AssetDatabase.FindAssets("package").Select(AssetDatabase.GUIDToAssetPath).Where(x => AssetDatabase.LoadAssetAtPath(x) != null).Select(PackageInfo.FindForAssetPath).Where(x => x != null).First(x => x.name == packageName);
+ }
+
internal void OnValidate()
{
if (NetworkConfig == null)
@@ -1030,6 +1048,8 @@ namespace Unity.Netcode
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += ModeChanged;
#endif
+ // Notify we have instantiated a new instance of NetworkManager.
+ OnInstantiated?.Invoke(this);
}
private void OnEnable()
@@ -1141,9 +1161,6 @@ namespace Unity.Netcode
UpdateTopology();
- //DANGOEXP TODO: Remove this before finalizing the experimental release
- NetworkConfig.AutoSpawnPlayerPrefabClientSide = DistributedAuthorityMode;
-
// Make sure the ServerShutdownState is reset when initializing
if (server)
{
@@ -1632,6 +1649,9 @@ namespace Unity.Netcode
UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded;
+ // Notify we are destroying NetworkManager
+ OnDestroying?.Invoke(this);
+
if (Singleton == this)
{
Singleton = null;
diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs
index f89bc1d..3ccd8ea 100644
--- a/Runtime/Core/NetworkObject.cs
+++ b/Runtime/Core/NetworkObject.cs
@@ -113,11 +113,6 @@ namespace Unity.Netcode
}
// Handle updating the currently active scene
- var networkObjects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None);
- foreach (var networkObject in networkObjects)
- {
- networkObject.OnValidate();
- }
NetworkObjectRefreshTool.ProcessActiveScene();
// Refresh all build settings scenes
@@ -130,14 +125,14 @@ namespace Unity.Netcode
continue;
}
// Add the scene to be processed
- NetworkObjectRefreshTool.ProcessScene(editorScene.path, false);
+ NetworkObjectRefreshTool.ProcessScene(editorScene.path, true);
}
// Process all added scenes
NetworkObjectRefreshTool.ProcessScenes();
}
- private void OnValidate()
+ internal void OnValidate()
{
// do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode
if (EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
@@ -229,6 +224,7 @@ namespace Unity.Netcode
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
{
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
+ EditorUtility.SetDirty(this);
}
IsSceneObject = true;
}
@@ -340,7 +336,7 @@ namespace Unity.Netcode
if (!HasAuthority)
{
- NetworkLog.LogError($"Only the authoirty can invoke {nameof(DeferDespawn)} and local Client-{NetworkManager.LocalClientId} is not the authority of {name}!");
+ NetworkLog.LogError($"Only the authority can invoke {nameof(DeferDespawn)} and local Client-{NetworkManager.LocalClientId} is not the authority of {name}!");
return;
}
@@ -1613,7 +1609,12 @@ namespace Unity.Netcode
}
else if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
{
- NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this);
+ // If spawning with observers or if not spawning with observers but the observer count is greater than 1 (i.e. owner/authority creating),
+ // then we want to send a spawn notification.
+ if (SpawnWithObservers || !SpawnWithObservers && Observers.Count > 1)
+ {
+ NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this);
+ }
}
else
{
@@ -2444,6 +2445,14 @@ namespace Unity.Netcode
}
}
+ internal void MarkOwnerReadVariablesDirty()
+ {
+ for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
+ {
+ ChildNetworkBehaviours[i].MarkOwnerReadVariablesDirty();
+ }
+ }
+
// 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
@@ -2770,11 +2779,11 @@ namespace Unity.Netcode
}
}
- internal void PostNetworkVariableWrite()
+ internal void PostNetworkVariableWrite(bool forced = false)
{
for (int k = 0; k < ChildNetworkBehaviours.Count; k++)
{
- ChildNetworkBehaviours[k].PostNetworkVariableWrite();
+ ChildNetworkBehaviours[k].PostNetworkVariableWrite(forced);
}
}
@@ -3053,10 +3062,15 @@ namespace Unity.Netcode
}
}
- // Add all known players to the observers list if they don't already exist
- foreach (var player in networkManager.SpawnManager.PlayerObjects)
+ // Only add all other players as observers if we are spawning with observers,
+ // otherwise user controls via NetworkShow.
+ if (networkObject.SpawnWithObservers)
{
- networkObject.Observers.Add(player.OwnerClientId);
+ // Add all known players to the observers list if they don't already exist
+ foreach (var player in networkManager.SpawnManager.PlayerObjects)
+ {
+ networkObject.Observers.Add(player.OwnerClientId);
+ }
}
}
}
diff --git a/Runtime/Core/NetworkObjectRefreshTool.cs b/Runtime/Core/NetworkObjectRefreshTool.cs
index b9c6db0..63d48e9 100644
--- a/Runtime/Core/NetworkObjectRefreshTool.cs
+++ b/Runtime/Core/NetworkObjectRefreshTool.cs
@@ -2,6 +2,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
+using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -21,6 +23,28 @@ namespace Unity.Netcode
internal static Action AllScenesProcessed;
+ internal static NetworkObject PrefabNetworkObject;
+
+ internal static void LogInfo(string msg, bool append = false)
+ {
+ if (!append)
+ {
+ s_Log.AppendLine(msg);
+ }
+ else
+ {
+ s_Log.Append(msg);
+ }
+ }
+
+ internal static void FlushLog()
+ {
+ Debug.Log(s_Log.ToString());
+ s_Log.Clear();
+ }
+
+ private static StringBuilder s_Log = new StringBuilder();
+
internal static void ProcessScene(string scenePath, bool processScenes = true)
{
if (!s_ScenesToUpdate.Contains(scenePath))
@@ -29,7 +53,10 @@ namespace Unity.Netcode
{
EditorSceneManager.sceneOpened += EditorSceneManager_sceneOpened;
EditorSceneManager.sceneSaved += EditorSceneManager_sceneSaved;
+ s_Log.Clear();
+ LogInfo("NetworkObject Refresh Scenes to Process:");
}
+ LogInfo($"[{scenePath}]", true);
s_ScenesToUpdate.Add(scenePath);
}
s_ProcessScenes = processScenes;
@@ -37,6 +64,7 @@ namespace Unity.Netcode
internal static void ProcessActiveScene()
{
+ FlushLog();
var activeScene = SceneManager.GetActiveScene();
if (s_ScenesToUpdate.Contains(activeScene.path) && s_ProcessScenes)
{
@@ -54,10 +82,12 @@ namespace Unity.Netcode
}
else
{
+ s_ProcessScenes = false;
s_CloseScenes = false;
EditorSceneManager.sceneSaved -= EditorSceneManager_sceneSaved;
EditorSceneManager.sceneOpened -= EditorSceneManager_sceneOpened;
AllScenesProcessed?.Invoke();
+ FlushLog();
}
}
@@ -68,9 +98,8 @@ namespace Unity.Netcode
// Provide a log of all scenes that were modified to the user
if (refreshed)
{
- Debug.Log($"Refreshed and saved updates to scene: {scene.name}");
+ LogInfo($"Refreshed and saved updates to scene: {scene.name}");
}
- s_ProcessScenes = false;
s_ScenesToUpdate.Remove(scene.path);
if (scene != SceneManager.GetActiveScene())
@@ -88,24 +117,41 @@ namespace Unity.Netcode
private static void SceneOpened(Scene scene)
{
+ LogInfo($"Processing scene {scene.name}:");
if (s_ScenesToUpdate.Contains(scene.path))
{
if (s_ProcessScenes)
{
- if (!EditorSceneManager.MarkSceneDirty(scene))
+ var prefabInstances = PrefabUtility.FindAllInstancesOfPrefab(PrefabNetworkObject.gameObject);
+
+ if (prefabInstances.Length > 0)
{
- Debug.Log($"Scene {scene.name} did not get marked as dirty!");
- FinishedProcessingScene(scene);
- }
- else
- {
- EditorSceneManager.SaveScene(scene);
+ var instancesSceneLoadedSpecific = prefabInstances.Where((c) => c.scene == scene).ToList();
+
+ if (instancesSceneLoadedSpecific.Count > 0)
+ {
+ foreach (var prefabInstance in instancesSceneLoadedSpecific)
+ {
+ prefabInstance.GetComponent().OnValidate();
+ }
+
+ if (!EditorSceneManager.MarkSceneDirty(scene))
+ {
+ LogInfo($"Scene {scene.name} did not get marked as dirty!");
+ FinishedProcessingScene(scene);
+ }
+ else
+ {
+ LogInfo($"Changes detected and applied!");
+ EditorSceneManager.SaveScene(scene);
+ }
+ return;
+ }
}
}
- else
- {
- FinishedProcessingScene(scene);
- }
+
+ LogInfo($"No changes required.");
+ FinishedProcessingScene(scene);
}
}
diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs
index 99010be..d418789 100644
--- a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs
+++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs
@@ -9,7 +9,6 @@ namespace Unity.Netcode
public ulong NetworkObjectId;
public ulong OwnerClientId;
- // DANGOEXP TODO: Remove these notes or change their format
// SERVICE NOTES:
// When forwarding the message to clients on the CMB Service side,
// you can set the ClientIdCount to 0 and skip writing the ClientIds.
@@ -258,15 +257,18 @@ namespace Unity.Netcode
continue;
}
- // If ownership is changing and this is not an ownership request approval then ignore the OnwerClientId
- // If it is just updating flags then ignore sending to the owner
- // If it is a request or approving request, then ignore the RequestClientId
- if ((OwnershipIsChanging && !RequestApproved && OwnerClientId == clientId) || (OwnershipFlagsUpdate && clientId == OwnerClientId)
- || ((RequestOwnership || RequestApproved) && clientId == RequestClientId))
+ // If ownership is changing and this is not an ownership request approval then ignore the SenderId
+ if (OwnershipIsChanging && !RequestApproved && context.SenderId == clientId)
{
continue;
}
+ // If it is just updating flags then ignore sending to the owner
+ // If it is a request or approving request, then ignore the RequestClientId
+ if ((OwnershipFlagsUpdate && clientId == OwnerClientId) || ((RequestOwnership || RequestApproved) && clientId == RequestClientId))
+ {
+ continue;
+ }
networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, clientId);
}
}
@@ -327,10 +329,12 @@ namespace Unity.Netcode
var networkManager = (NetworkManager)context.SystemOwner;
var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId];
- // DANGO-TODO: This probably shouldn't be allowed to happen.
+ // Sanity check that we are not sending duplicated change ownership messages
if (networkObject.OwnerClientId == OwnerClientId)
{
- UnityEngine.Debug.LogWarning($"Unnecessary ownership changed message for {NetworkObjectId}");
+ UnityEngine.Debug.LogError($"Unnecessary ownership changed message for {NetworkObjectId}.");
+ // Ignore the message
+ return;
}
var originalOwner = networkObject.OwnerClientId;
@@ -347,12 +351,6 @@ namespace Unity.Netcode
networkObject.InvokeBehaviourOnLostOwnership();
}
- // We are new owner or (client-server) or running in distributed authority mode
- if (OwnerClientId == networkManager.LocalClientId || networkManager.DistributedAuthorityMode)
- {
- networkObject.InvokeBehaviourOnGainedOwnership();
- }
-
// If in distributed authority mode
if (networkManager.DistributedAuthorityMode)
{
@@ -374,6 +372,22 @@ namespace Unity.Netcode
}
}
+ // We are new owner or (client-server) or running in distributed authority mode
+ if (OwnerClientId == networkManager.LocalClientId || networkManager.DistributedAuthorityMode)
+ {
+ networkObject.InvokeBehaviourOnGainedOwnership();
+ }
+
+
+ if (originalOwner == networkManager.LocalClientId && !networkManager.DistributedAuthorityMode)
+ {
+ // Mark any owner read variables as dirty
+ networkObject.MarkOwnerReadVariablesDirty();
+ // Immediately queue any pending deltas and order the message before the
+ // change in ownership message.
+ networkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
+ }
+
// Always invoke ownership change notifications
networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId);
diff --git a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs
index bb3e446..7b9a87f 100644
--- a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs
+++ b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs
@@ -3,14 +3,39 @@ using Unity.Collections;
namespace Unity.Netcode
{
+ internal struct ServiceConfig : INetworkSerializable
+ {
+ public uint Version;
+ public bool IsRestoredSession;
+ public ulong CurrentSessionOwner;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ if (serializer.IsWriter)
+ {
+ BytePacker.WriteValueBitPacked(serializer.GetFastBufferWriter(), Version);
+ serializer.SerializeValue(ref IsRestoredSession);
+ BytePacker.WriteValueBitPacked(serializer.GetFastBufferWriter(), CurrentSessionOwner);
+ }
+ else
+ {
+ ByteUnpacker.ReadValueBitPacked(serializer.GetFastBufferReader(), out Version);
+ serializer.SerializeValue(ref IsRestoredSession);
+ ByteUnpacker.ReadValueBitPacked(serializer.GetFastBufferReader(), out CurrentSessionOwner);
+ }
+ }
+ }
+
internal struct ConnectionApprovedMessage : INetworkMessage
{
+ private const int k_AddCMBServiceConfig = 2;
private const int k_VersionAddClientIds = 1;
- public int Version => k_VersionAddClientIds;
+ public int Version => k_AddCMBServiceConfig;
public ulong OwnerClientId;
public int NetworkTick;
// The cloud state service should set this if we are restoring a session
+ public ServiceConfig ServiceConfig;
public bool IsRestoredSession;
public ulong CurrentSessionOwner;
// Not serialized
@@ -25,6 +50,32 @@ namespace Unity.Netcode
public NativeArray ConnectedClientIds;
+ private int m_ReceiveMessageVersion;
+
+ private ulong GetSessionOwner()
+ {
+ if (m_ReceiveMessageVersion >= k_AddCMBServiceConfig)
+ {
+ return ServiceConfig.CurrentSessionOwner;
+ }
+ else
+ {
+ return CurrentSessionOwner;
+ }
+ }
+
+ private bool GetIsSessionRestor()
+ {
+ if (m_ReceiveMessageVersion >= k_AddCMBServiceConfig)
+ {
+ return ServiceConfig.IsRestoredSession;
+ }
+ else
+ {
+ return IsRestoredSession;
+ }
+ }
+
public void Serialize(FastBufferWriter writer, int targetVersion)
{
// ============================================================
@@ -45,8 +96,17 @@ namespace Unity.Netcode
BytePacker.WriteValueBitPacked(writer, NetworkTick);
if (IsDistributedAuthority)
{
- writer.WriteValueSafe(IsRestoredSession);
- BytePacker.WriteValueBitPacked(writer, CurrentSessionOwner);
+ if (targetVersion >= k_AddCMBServiceConfig)
+ {
+ ServiceConfig.IsRestoredSession = false;
+ ServiceConfig.CurrentSessionOwner = CurrentSessionOwner;
+ writer.WriteNetworkSerializable(ServiceConfig);
+ }
+ else
+ {
+ writer.WriteValueSafe(IsRestoredSession);
+ BytePacker.WriteValueBitPacked(writer, CurrentSessionOwner);
+ }
}
if (targetVersion >= k_VersionAddClientIds)
@@ -122,13 +182,20 @@ namespace Unity.Netcode
// ============================================================
// END FORBIDDEN SEGMENT
// ============================================================
-
+ m_ReceiveMessageVersion = receivedMessageVersion;
ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId);
ByteUnpacker.ReadValueBitPacked(reader, out NetworkTick);
if (networkManager.DistributedAuthorityMode)
{
- reader.ReadValueSafe(out IsRestoredSession);
- ByteUnpacker.ReadValueBitPacked(reader, out CurrentSessionOwner);
+ if (receivedMessageVersion >= k_AddCMBServiceConfig)
+ {
+ reader.ReadNetworkSerializable(out ServiceConfig);
+ }
+ else
+ {
+ reader.ReadValueSafe(out IsRestoredSession);
+ ByteUnpacker.ReadValueBitPacked(reader, out CurrentSessionOwner);
+ }
}
if (receivedMessageVersion >= k_VersionAddClientIds)
@@ -157,7 +224,7 @@ namespace Unity.Netcode
if (networkManager.DistributedAuthorityMode)
{
- networkManager.SetSessionOwner(CurrentSessionOwner);
+ networkManager.SetSessionOwner(GetSessionOwner());
if (networkManager.LocalClient.IsSessionOwner && networkManager.NetworkConfig.EnableSceneManagement)
{
networkManager.SceneManager.InitializeScenesLoaded();
@@ -233,9 +300,9 @@ namespace Unity.Netcode
// Mark the client being connected
networkManager.IsConnectedClient = true;
- networkManager.SceneManager.IsRestoringSession = IsRestoredSession;
+ networkManager.SceneManager.IsRestoringSession = GetIsSessionRestor();
- if (!IsRestoredSession)
+ if (!networkManager.SceneManager.IsRestoringSession)
{
// Synchronize the service with the initial session owner's loaded scenes and spawned objects
networkManager.SceneManager.SynchronizeNetworkObjects(NetworkManager.ServerClientId);
diff --git a/Runtime/Messaging/Messages/ConnectionRequestMessage.cs b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs
index 790791d..d8e60d2 100644
--- a/Runtime/Messaging/Messages/ConnectionRequestMessage.cs
+++ b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs
@@ -2,16 +2,54 @@ using Unity.Collections;
namespace Unity.Netcode
{
- internal struct ConnectionRequestMessage : INetworkMessage
+ ///
+ /// Only used when connecting to the distributed authority service
+ ///
+ internal struct ClientConfig : INetworkSerializable
{
- public int Version => 0;
-
- public ulong ConfigHash;
-
- public bool CMBServiceConnection;
+ ///
+ /// We start at version 1, where anything less than version 1 on the service side
+ /// is not bypass feature compatible.
+ ///
+ private const int k_BypassFeatureCompatible = 1;
+ public int Version => k_BypassFeatureCompatible;
public uint TickRate;
public bool EnableSceneManagement;
+ // Only gets deserialized but should never be used unless testing
+ public int RemoteClientVersion;
+
+ public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
+ {
+ if (serializer.IsWriter)
+ {
+ var writer = serializer.GetFastBufferWriter();
+ BytePacker.WriteValueBitPacked(writer, Version);
+ BytePacker.WriteValueBitPacked(writer, TickRate);
+ writer.WriteValueSafe(EnableSceneManagement);
+ }
+ else
+ {
+ var reader = serializer.GetFastBufferReader();
+ ByteUnpacker.ReadValueBitPacked(reader, out RemoteClientVersion);
+ ByteUnpacker.ReadValueBitPacked(reader, out TickRate);
+ reader.ReadValueSafe(out EnableSceneManagement);
+ }
+ }
+ }
+
+ internal struct ConnectionRequestMessage : INetworkMessage
+ {
+ // This version update is unidirectional (client to service) and version
+ // handling occurs on the service side. This serialized data is never sent
+ // to a host or server.
+ private const int k_SendClientConfigToService = 1;
+ public int Version => k_SendClientConfigToService;
+
+ public ulong ConfigHash;
+ public bool CMBServiceConnection;
+ public ClientConfig ClientConfig;
+
public byte[] ConnectionData;
public bool ShouldSendConnectionData;
@@ -36,8 +74,7 @@ namespace Unity.Netcode
if (CMBServiceConnection)
{
- writer.WriteValueSafe(TickRate);
- writer.WriteValueSafe(EnableSceneManagement);
+ writer.WriteNetworkSerializable(ClientConfig);
}
if (ShouldSendConnectionData)
diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs
index eb6050e..8db084c 100644
--- a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs
+++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using Unity.Collections;
namespace Unity.Netcode
@@ -10,9 +11,22 @@ namespace Unity.Netcode
/// serialization. This is due to the generally amorphous nature of network variable
/// deltas, since they're all driven by custom virtual method overloads.
///
+ ///
+ /// Version 1:
+ /// This version -does not- use the "KeepDirty" approach. Instead, the server will forward any state updates
+ /// to the connected clients that are not the sender or the server itself. Each NetworkVariable state update
+ /// included, on a per client basis, is first validated that the client can read the NetworkVariable before
+ /// being added to the m_ForwardUpdates table.
+ /// Version 0:
+ /// The original version uses the "KeepDirty" approach in a client-server network topology where the server
+ /// proxies state updates by "keeping the NetworkVariable(s) dirty" so it will send state updates
+ /// at the end of the frame (but could delay until the next tick).
+ ///
internal struct NetworkVariableDeltaMessage : INetworkMessage
{
- public int Version => 0;
+ private const int k_ServerDeltaForwardingAndNetworkDelivery = 1;
+ public int Version => k_ServerDeltaForwardingAndNetworkDelivery;
+
public ulong NetworkObjectId;
public ushort NetworkBehaviourIndex;
@@ -21,10 +35,62 @@ namespace Unity.Netcode
public ulong TargetClientId;
public NetworkBehaviour NetworkBehaviour;
+ public NetworkDelivery NetworkDelivery;
+
private FastBufferReader m_ReceivedNetworkVariableData;
+ private bool m_ForwardingMessage;
+
+ private int m_ReceivedMessageVersion;
+
private const string k_Name = "NetworkVariableDeltaMessage";
+ private Dictionary> m_ForwardUpdates;
+
+ private List m_UpdatedNetworkVariables;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteNetworkVariable(ref FastBufferWriter writer, ref NetworkVariableBase networkVariable, bool distributedAuthorityMode, bool ensureNetworkVariableLengthSafety, int nonfragmentedSize, int fragmentedSize)
+ {
+ if (ensureNetworkVariableLengthSafety)
+ {
+ var tempWriter = new FastBufferWriter(nonfragmentedSize, Allocator.Temp, fragmentedSize);
+ networkVariable.WriteDelta(tempWriter);
+ BytePacker.WriteValueBitPacked(writer, tempWriter.Length);
+
+ if (!writer.TryBeginWrite(tempWriter.Length))
+ {
+ throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}");
+ }
+
+ tempWriter.CopyTo(writer);
+ }
+ else
+ {
+ // TODO: Determine if we need to remove this with the 6.1 service updates
+ if (distributedAuthorityMode)
+ {
+ var size_marker = writer.Position;
+ writer.WriteValueSafe(0);
+ var start_marker = writer.Position;
+ networkVariable.WriteDelta(writer);
+ var end_marker = writer.Position;
+ writer.Seek(size_marker);
+ var size = end_marker - start_marker;
+ if (size == 0)
+ {
+ UnityEngine.Debug.LogError($"Invalid write size of zero!");
+ }
+ writer.WriteValueSafe((ushort)size);
+ writer.Seek(end_marker);
+ }
+ else
+ {
+ networkVariable.WriteDelta(writer);
+ }
+ }
+ }
+
public void Serialize(FastBufferWriter writer, int targetVersion)
{
if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(NetworkObjectId) + FastBufferWriter.GetWriteSize(NetworkBehaviourIndex)))
@@ -34,10 +100,67 @@ namespace Unity.Netcode
var obj = NetworkBehaviour.NetworkObject;
var networkManager = obj.NetworkManagerOwner;
+ var typeName = NetworkBehaviour.__getTypeName();
+ var nonFragmentedMessageMaxSize = networkManager.MessageManager.NonFragmentedMessageMaxSize;
+ var fragmentedMessageMaxSize = networkManager.MessageManager.FragmentedMessageMaxSize;
+ var ensureNetworkVariableLengthSafety = networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety;
+ var distributedAuthorityMode = networkManager.DistributedAuthorityMode;
BytePacker.WriteValueBitPacked(writer, NetworkObjectId);
BytePacker.WriteValueBitPacked(writer, NetworkBehaviourIndex);
- if (networkManager.DistributedAuthorityMode)
+
+ // If using k_IncludeNetworkDelivery version, then we want to write the network delivery used and if we
+ // are forwarding state updates then serialize any NetworkVariable states specific to this client.
+ if (targetVersion >= k_ServerDeltaForwardingAndNetworkDelivery)
+ {
+ writer.WriteValueSafe(NetworkDelivery);
+ // If we are forwarding the message, then proceed to forward state updates specific to the targeted client
+ if (m_ForwardingMessage)
+ {
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode)
+ {
+ writer.WriteValueSafe((ushort)NetworkBehaviour.NetworkVariableFields.Count);
+ }
+
+ for (int i = 0; i < NetworkBehaviour.NetworkVariableFields.Count; i++)
+ {
+ var startingSize = writer.Length;
+ var networkVariable = NetworkBehaviour.NetworkVariableFields[i];
+ var shouldWrite = m_ForwardUpdates[TargetClientId].Contains(i);
+
+ // This var does not belong to the currently iterating delivery group.
+ if (distributedAuthorityMode)
+ {
+ if (!shouldWrite)
+ {
+ writer.WriteValueSafe(0);
+ }
+ }
+ else if (ensureNetworkVariableLengthSafety)
+ {
+ if (!shouldWrite)
+ {
+ BytePacker.WriteValueBitPacked(writer, (ushort)0);
+ }
+ }
+ else
+ {
+ writer.WriteValueSafe(shouldWrite);
+ }
+
+ if (shouldWrite)
+ {
+ WriteNetworkVariable(ref writer, ref networkVariable, distributedAuthorityMode, ensureNetworkVariableLengthSafety, nonFragmentedMessageMaxSize, fragmentedMessageMaxSize);
+ networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(TargetClientId, obj, networkVariable.Name, typeName, writer.Length - startingSize);
+ }
+ }
+ return;
+ }
+ }
+
+ // DANGO TODO: Remove this when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode)
{
writer.WriteValueSafe((ushort)NetworkBehaviour.NetworkVariableFields.Count);
}
@@ -46,12 +169,12 @@ namespace Unity.Netcode
{
if (!DeliveryMappedNetworkVariableIndex.Contains(i))
{
- // This var does not belong to the currently iterating delivery group.
- if (networkManager.DistributedAuthorityMode)
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode)
{
writer.WriteValueSafe(0);
}
- else if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
+ else if (ensureNetworkVariableLengthSafety)
{
BytePacker.WriteValueBitPacked(writer, (ushort)0);
}
@@ -88,14 +211,15 @@ namespace Unity.Netcode
shouldWrite = false;
}
- if (networkManager.DistributedAuthorityMode)
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode)
{
if (!shouldWrite)
{
writer.WriteValueSafe(0);
}
}
- else if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
+ else if (ensureNetworkVariableLengthSafety)
{
if (!shouldWrite)
{
@@ -109,53 +233,22 @@ namespace Unity.Netcode
if (shouldWrite)
{
- if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
- {
- var tempWriter = new FastBufferWriter(networkManager.MessageManager.NonFragmentedMessageMaxSize, Allocator.Temp, networkManager.MessageManager.FragmentedMessageMaxSize);
- NetworkBehaviour.NetworkVariableFields[i].WriteDelta(tempWriter);
- BytePacker.WriteValueBitPacked(writer, tempWriter.Length);
-
- if (!writer.TryBeginWrite(tempWriter.Length))
- {
- throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}");
- }
-
- tempWriter.CopyTo(writer);
- }
- else
- {
- if (networkManager.DistributedAuthorityMode)
- {
- var size_marker = writer.Position;
- writer.WriteValueSafe(0);
- var start_marker = writer.Position;
- networkVariable.WriteDelta(writer);
- var end_marker = writer.Position;
- writer.Seek(size_marker);
- var size = end_marker - start_marker;
- writer.WriteValueSafe((ushort)size);
- writer.Seek(end_marker);
- }
- else
- {
- networkVariable.WriteDelta(writer);
- }
- }
- networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(
- TargetClientId,
- obj,
- networkVariable.Name,
- NetworkBehaviour.__getTypeName(),
- writer.Length - startingSize);
+ WriteNetworkVariable(ref writer, ref networkVariable, distributedAuthorityMode, ensureNetworkVariableLengthSafety, nonFragmentedMessageMaxSize, fragmentedMessageMaxSize);
+ networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent(TargetClientId, obj, networkVariable.Name, typeName, writer.Length - startingSize);
}
}
}
public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion)
{
+ m_ReceivedMessageVersion = receivedMessageVersion;
ByteUnpacker.ReadValueBitPacked(reader, out NetworkObjectId);
ByteUnpacker.ReadValueBitPacked(reader, out NetworkBehaviourIndex);
-
+ // If we are using the k_IncludeNetworkDelivery message version, then read the NetworkDelivery used
+ if (receivedMessageVersion >= k_ServerDeltaForwardingAndNetworkDelivery)
+ {
+ reader.ReadValueSafe(out NetworkDelivery);
+ }
m_ReceivedNetworkVariableData = reader;
return true;
@@ -167,7 +260,12 @@ namespace Unity.Netcode
if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out NetworkObject networkObject))
{
+ var distributedAuthorityMode = networkManager.DistributedAuthorityMode;
+ var ensureNetworkVariableLengthSafety = networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety;
var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex);
+ var isServerAndDeltaForwarding = m_ReceivedMessageVersion >= k_ServerDeltaForwardingAndNetworkDelivery && networkManager.IsServer;
+ var markNetworkVariableDirty = m_ReceivedMessageVersion >= k_ServerDeltaForwardingAndNetworkDelivery ? false : networkManager.IsServer;
+ m_UpdatedNetworkVariables = new List();
if (networkBehaviour == null)
{
@@ -178,7 +276,8 @@ namespace Unity.Netcode
}
else
{
- if (networkManager.DistributedAuthorityMode)
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode)
{
m_ReceivedNetworkVariableData.ReadValueSafe(out ushort variableCount);
if (variableCount != networkBehaviour.NetworkVariableFields.Count)
@@ -187,10 +286,30 @@ namespace Unity.Netcode
}
}
+ // (For client-server) As opposed to worrying about adding additional processing on the server to send NetworkVariable
+ // updates at the end of the frame, we now track all NetworkVariable state updates, per client, that need to be forwarded
+ // to the client. This creates a list of all remaining connected clients that could have updates applied.
+ if (isServerAndDeltaForwarding)
+ {
+ m_ForwardUpdates = new Dictionary>();
+ foreach (var clientId in networkManager.ConnectedClientsIds)
+ {
+ if (clientId == context.SenderId || clientId == networkManager.LocalClientId || !networkObject.Observers.Contains(clientId))
+ {
+ continue;
+ }
+ m_ForwardUpdates.Add(clientId, new List());
+ }
+ }
+
+ // Update NetworkVariable Fields
for (int i = 0; i < networkBehaviour.NetworkVariableFields.Count; i++)
{
int varSize = 0;
- if (networkManager.DistributedAuthorityMode)
+ var networkVariable = networkBehaviour.NetworkVariableFields[i];
+
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode)
{
m_ReceivedNetworkVariableData.ReadValueSafe(out ushort variableSize);
varSize = variableSize;
@@ -200,10 +319,9 @@ namespace Unity.Netcode
continue;
}
}
- else if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
+ else if (ensureNetworkVariableLengthSafety)
{
ByteUnpacker.ReadValueBitPacked(m_ReceivedNetworkVariableData, out varSize);
-
if (varSize == 0)
{
continue;
@@ -218,8 +336,6 @@ namespace Unity.Netcode
}
}
- 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
@@ -247,13 +363,58 @@ namespace Unity.Netcode
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;
- // Read Delta so we also notify any subscribers to a change in the NetworkVariable
- networkVariable.ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer);
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode || ensureNetworkVariableLengthSafety)
+ {
+ var remainingBufferSize = m_ReceivedNetworkVariableData.Length - m_ReceivedNetworkVariableData.Position;
+ if (varSize > (remainingBufferSize))
+ {
+ UnityEngine.Debug.LogError($"[{networkBehaviour.name}][Delta State Read Error] Expecting to read {varSize} but only {remainingBufferSize} remains!");
+ return;
+ }
+ }
+
+ // Added a try catch here to assure any failure will only fail on this one message and not disrupt the stack
+ try
+ {
+ // Read the delta
+ networkVariable.ReadDelta(m_ReceivedNetworkVariableData, markNetworkVariableDirty);
+
+ // Add the NetworkVariable field index so we can invoke the PostDeltaRead
+ m_UpdatedNetworkVariables.Add(i);
+ }
+ catch (Exception ex)
+ {
+ UnityEngine.Debug.LogException(ex);
+ return;
+ }
+
+ // (For client-server) As opposed to worrying about adding additional processing on the server to send NetworkVariable
+ // updates at the end of the frame, we now track all NetworkVariable state updates, per client, that need to be forwarded
+ // to the client. This happens once the server is finished processing all state updates for this message.
+ if (isServerAndDeltaForwarding)
+ {
+ foreach (var forwardEntry in m_ForwardUpdates)
+ {
+ // Only track things that the client can read
+ if (networkVariable.CanClientRead(forwardEntry.Key))
+ {
+ // If the object is about to be shown to the client then don't send an update as it will
+ // send a full update when shown.
+ if (networkManager.SpawnManager.ObjectsToShowToClient.ContainsKey(forwardEntry.Key) &&
+ networkManager.SpawnManager.ObjectsToShowToClient[forwardEntry.Key]
+ .Contains(networkObject))
+ {
+ continue;
+ }
+ forwardEntry.Value.Add(i);
+ }
+ }
+ }
networkManager.NetworkMetrics.TrackNetworkVariableDeltaReceived(
context.SenderId,
@@ -262,7 +423,8 @@ namespace Unity.Netcode
networkBehaviour.__getTypeName(),
context.MessageSize);
- if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety || networkManager.DistributedAuthorityMode)
+ // DANGO TODO: Remove distributedAuthorityMode portion when we remove the service specific NetworkVariable stuff
+ if (distributedAuthorityMode || ensureNetworkVariableLengthSafety)
{
if (m_ReceivedNetworkVariableData.Position > (readStartPos + varSize))
{
@@ -284,6 +446,40 @@ namespace Unity.Netcode
}
}
}
+
+ // If we are using the version of this message that includes network delivery, then
+ // forward this update to all connected clients (other than the sender and the server).
+ if (isServerAndDeltaForwarding)
+ {
+ var message = new NetworkVariableDeltaMessage()
+ {
+ NetworkBehaviour = networkBehaviour,
+ NetworkBehaviourIndex = NetworkBehaviourIndex,
+ NetworkObjectId = NetworkObjectId,
+ m_ForwardingMessage = true,
+ m_ForwardUpdates = m_ForwardUpdates,
+ };
+
+ foreach (var forwardEntry in m_ForwardUpdates)
+ {
+ // Only forward updates to any client that has visibility to the state updates included in this message
+ if (forwardEntry.Value.Count > 0)
+ {
+ message.TargetClientId = forwardEntry.Key;
+ networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery, forwardEntry.Key);
+ }
+ }
+ }
+
+ // This should be always invoked (client & server) to assure the previous values are set
+ // !! IMPORTANT ORDER OF OPERATIONS !! (Has to happen after forwarding deltas)
+ // When a server forwards delta updates to connected clients, it needs to preserve the previous value
+ // until it is done serializing all valid NetworkVariable field deltas (relative to each client). This
+ // is invoked after it is done forwarding the deltas.
+ foreach (var fieldIndex in m_UpdatedNetworkVariables)
+ {
+ networkBehaviour.NetworkVariableFields[fieldIndex].PostDeltaRead();
+ }
}
}
else
diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs
index 130cf4e..203632c 100644
--- a/Runtime/NetworkVariable/Collections/NetworkList.cs
+++ b/Runtime/NetworkVariable/Collections/NetworkList.cs
@@ -177,6 +177,13 @@ namespace Unity.Netcode
///
public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
{
+ /// This is only invoked by and the only time
+ /// keepDirtyDelta is set is when it is the server processing. To be able to handle previous
+ /// versions, we use IsServer to keep the dirty states received and the keepDirtyDelta to
+ /// actually mark this as dirty and add it to the list of s to
+ /// be updated. With the forwarding of deltas being handled by ,
+ /// once all clients have been forwarded the dirty events, we clear them by invoking .
+ var isServer = m_NetworkManager.IsServer;
reader.ReadValueSafe(out ushort deltaCount);
for (int i = 0; i < deltaCount; i++)
{
@@ -199,7 +206,7 @@ namespace Unity.Netcode
});
}
- if (keepDirtyDelta)
+ if (isServer)
{
m_DirtyEvents.Add(new NetworkListEvent()
{
@@ -207,7 +214,11 @@ namespace Unity.Netcode
Index = m_List.Length - 1,
Value = m_List[m_List.Length - 1]
});
- MarkNetworkObjectDirty();
+ // Preserve the legacy way of handling this
+ if (keepDirtyDelta)
+ {
+ MarkNetworkObjectDirty();
+ }
}
}
break;
@@ -237,7 +248,7 @@ namespace Unity.Netcode
});
}
- if (keepDirtyDelta)
+ if (isServer)
{
m_DirtyEvents.Add(new NetworkListEvent()
{
@@ -245,7 +256,11 @@ namespace Unity.Netcode
Index = index,
Value = m_List[index]
});
- MarkNetworkObjectDirty();
+ // Preserve the legacy way of handling this
+ if (keepDirtyDelta)
+ {
+ MarkNetworkObjectDirty();
+ }
}
}
break;
@@ -271,7 +286,7 @@ namespace Unity.Netcode
});
}
- if (keepDirtyDelta)
+ if (isServer)
{
m_DirtyEvents.Add(new NetworkListEvent()
{
@@ -279,7 +294,11 @@ namespace Unity.Netcode
Index = index,
Value = value
});
- MarkNetworkObjectDirty();
+ // Preserve the legacy way of handling this
+ if (keepDirtyDelta)
+ {
+ MarkNetworkObjectDirty();
+ }
}
}
break;
@@ -299,7 +318,7 @@ namespace Unity.Netcode
});
}
- if (keepDirtyDelta)
+ if (isServer)
{
m_DirtyEvents.Add(new NetworkListEvent()
{
@@ -307,7 +326,11 @@ namespace Unity.Netcode
Index = index,
Value = value
});
- MarkNetworkObjectDirty();
+ // Preserve the legacy way of handling this
+ if (keepDirtyDelta)
+ {
+ MarkNetworkObjectDirty();
+ }
}
}
break;
@@ -335,7 +358,7 @@ namespace Unity.Netcode
});
}
- if (keepDirtyDelta)
+ if (isServer)
{
m_DirtyEvents.Add(new NetworkListEvent()
{
@@ -344,7 +367,11 @@ namespace Unity.Netcode
Value = value,
PreviousValue = previousValue
});
- MarkNetworkObjectDirty();
+ // Preserve the legacy way of handling this
+ if (keepDirtyDelta)
+ {
+ MarkNetworkObjectDirty();
+ }
}
}
break;
@@ -361,13 +388,18 @@ namespace Unity.Netcode
});
}
- if (keepDirtyDelta)
+ if (isServer)
{
m_DirtyEvents.Add(new NetworkListEvent()
{
Type = eventType
});
- MarkNetworkObjectDirty();
+
+ // Preserve the legacy way of handling this
+ if (keepDirtyDelta)
+ {
+ MarkNetworkObjectDirty();
+ }
}
}
break;
@@ -381,6 +413,18 @@ namespace Unity.Netcode
}
}
+ ///
+ ///
+ /// For NetworkList, we just need to reset dirty if a server has read deltas
+ ///
+ internal override void PostDeltaRead()
+ {
+ if (m_NetworkManager.IsServer)
+ {
+ ResetDirty();
+ }
+ }
+
///
public IEnumerator GetEnumerator()
{
diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs
index a98197d..16ca42a 100644
--- a/Runtime/NetworkVariable/NetworkVariable.cs
+++ b/Runtime/NetworkVariable/NetworkVariable.cs
@@ -41,6 +41,7 @@ namespace Unity.Netcode
base.OnInitialize();
m_HasPreviousValue = true;
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue);
}
@@ -58,6 +59,7 @@ namespace Unity.Netcode
: base(readPerm, writePerm)
{
m_InternalValue = value;
+ m_InternalOriginalValue = default;
// Since we start with IsDirty = true, this doesn't need to be duplicated
// right away. It won't get read until after ResetDirty() is called, and
// the duplicate will be made there. Avoiding calling
@@ -76,6 +78,7 @@ namespace Unity.Netcode
if (m_NetworkBehaviour == null || m_NetworkBehaviour != null && !m_NetworkBehaviour.NetworkObject.IsSpawned)
{
m_InternalValue = value;
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
m_PreviousValue = default;
}
}
@@ -86,6 +89,12 @@ namespace Unity.Netcode
[SerializeField]
private protected T m_InternalValue;
+ // The introduction of standard .NET collections caused an issue with permissions since there is no way to detect changes in the
+ // collection without doing a full comparison. While this approach does consume more memory per collection instance, it is the
+ // lowest risk approach to resolving the issue where a client with no write permissions could make changes to a collection locally
+ // which can cause a myriad of issues.
+ private protected T m_InternalOriginalValue;
+
private protected T m_PreviousValue;
private bool m_HasPreviousValue;
@@ -116,6 +125,7 @@ namespace Unity.Netcode
{
T previousValue = m_InternalValue;
m_InternalValue = value;
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
SetDirty(true);
m_IsDisposed = false;
OnValueChanged?.Invoke(previousValue, m_InternalValue);
@@ -136,6 +146,17 @@ namespace Unity.Netcode
{
var isDirty = base.IsDirty();
+ // A client without permissions invoking this method should only check to assure the current value is equal to the last known current value
+ if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId))
+ {
+ // If modifications are detected, then revert back to the last known current value
+ if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue))
+ {
+ NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
+ }
+ return false;
+ }
+
// Compare the previous with the current if not dirty or forcing a check.
if ((!isDirty || forceCheck) && !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue))
{
@@ -166,6 +187,7 @@ namespace Unity.Netcode
}
m_InternalValue = default;
+ m_InternalOriginalValue = default;
if (m_HasPreviousValue && m_PreviousValue is IDisposable previousValueDisposable)
{
m_HasPreviousValue = false;
@@ -188,6 +210,13 @@ namespace Unity.Netcode
/// Whether or not the container is dirty
public override bool IsDirty()
{
+ // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert
+ // to the original collection value prior to applying updates (primarily for collections).
+ if (!NetworkUpdaterCheck && m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue))
+ {
+ NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
+ return true;
+ }
// For most cases we can use the dirty flag.
// This doesn't work for cases where we're wrapping more complex types
// like INetworkSerializable, NativeList, NativeArray, etc.
@@ -199,11 +228,11 @@ namespace Unity.Netcode
return true;
}
+ var dirty = !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue);
// Cache the dirty value so we don't perform this again if we already know we're dirty
// Unfortunately we can't cache the NOT dirty state, because that might change
// in between to checks... but the DIRTY state won't change until ResetDirty()
// is called.
- var dirty = !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue);
SetDirty(dirty);
return dirty;
}
@@ -221,6 +250,8 @@ namespace Unity.Netcode
{
m_HasPreviousValue = true;
NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue);
+ // Once updated, assure the original current value is updated for future comparison purposes
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
}
base.ResetDirty();
}
@@ -241,16 +272,20 @@ namespace Unity.Netcode
/// Whether or not the container should keep the dirty delta, or mark the delta as consumed
public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
{
- // In order to get managed collections to properly have a previous and current value, we have to
- // duplicate the collection at this point before making any modifications to the current.
- m_HasPreviousValue = true;
- NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue);
+ // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert
+ // to the original collection value prior to applying updates (primarily for collections).
+ if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue))
+ {
+ NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
+ }
+
NetworkVariableSerialization.ReadDelta(reader, ref m_InternalValue);
- // todo:
// keepDirtyDelta marks a variable received as dirty and causes the server to send the value to clients
// In a prefect world, whether a variable was A) modified locally or B) received and needs retransmit
// would be stored in different fields
+ // LEGACY NOTE: This is only to handle NetworkVariableDeltaMessage Version 0 connections. The updated
+ // NetworkVariableDeltaMessage no longer uses this approach.
if (keepDirtyDelta)
{
SetDirty(true);
@@ -259,10 +294,43 @@ namespace Unity.Netcode
OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue);
}
+ ///
+ /// This should be always invoked (client & server) to assure the previous values are set
+ /// !! IMPORTANT !!
+ /// When a server forwards delta updates to connected clients, it needs to preserve the previous dirty value(s)
+ /// until it is done serializing all valid NetworkVariable field deltas (relative to each client). This is invoked
+ /// after it is done forwarding the deltas at the end of the method.
+ ///
+ internal override void PostDeltaRead()
+ {
+ // In order to get managed collections to properly have a previous and current value, we have to
+ // duplicate the collection at this point before making any modifications to the current.
+ m_HasPreviousValue = true;
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue);
+ // Once updated, assure the original current value is updated for future comparison purposes
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
+ }
+
///
public override void ReadField(FastBufferReader reader)
{
+ // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert
+ // to the original collection value prior to applying updates (primarily for collections).
+ if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue))
+ {
+ NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue);
+ }
+
NetworkVariableSerialization.Read(reader, ref m_InternalValue);
+ // In order to get managed collections to properly have a previous and current value, we have to
+ // duplicate the collection at this point before making any modifications to the current.
+ // We duplicate the final value after the read (for ReadField ONLY) so the previous value is at par
+ // with the current value (since this is only invoked when initially synchronizing).
+ m_HasPreviousValue = true;
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue);
+
+ // Once updated, assure the original current value is updated for future comparison purposes
+ NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue);
}
///
@@ -270,5 +338,20 @@ namespace Unity.Netcode
{
NetworkVariableSerialization.Write(writer, ref m_InternalValue);
}
+
+ internal override void WriteFieldSynchronization(FastBufferWriter writer)
+ {
+ // If we have a pending update, then synchronize the client with the previously known
+ // value since the updated version will be sent on the next tick or next time it is
+ // set to be updated
+ if (base.IsDirty() && m_HasPreviousValue)
+ {
+ NetworkVariableSerialization.Write(writer, ref m_PreviousValue);
+ }
+ else
+ {
+ base.WriteFieldSynchronization(writer);
+ }
+ }
}
}
diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs
index 75ce48a..8802397 100644
--- a/Runtime/NetworkVariable/NetworkVariableBase.cs
+++ b/Runtime/NetworkVariable/NetworkVariableBase.cs
@@ -251,6 +251,12 @@ namespace Unity.Netcode
m_IsDirty = false;
}
+ ///
+ /// Only used during the NetworkBehaviourUpdater pass and only used for NetworkVariable.
+ /// This is to bypass duplication of the "original internal value" for collections.
+ ///
+ internal bool NetworkUpdaterCheck;
+
///
/// Gets Whether or not the container is dirty
///
@@ -341,6 +347,32 @@ namespace Unity.Netcode
/// Whether or not the delta should be kept as dirty or consumed
public abstract void ReadDelta(FastBufferReader reader, bool keepDirtyDelta);
+ ///
+ /// This should be always invoked (client & server) to assure the previous values are set
+ /// !! IMPORTANT !!
+ /// When a server forwards delta updates to connected clients, it needs to preserve the previous dirty value(s)
+ /// until it is done serializing all valid NetworkVariable field deltas (relative to each client). This is invoked
+ /// after it is done forwarding the deltas at the end of the method.
+ ///
+ internal virtual void PostDeltaRead()
+ {
+ }
+
+ ///
+ /// There are scenarios, specifically with collections, where a client could be synchronizing and
+ /// some NetworkVariables have pending updates. To avoid duplicating entries, this is invoked only
+ /// when sending the full synchronization information.
+ ///
+ ///
+ /// Derrived classes should send the previous value for synchronization so when the updated value
+ /// is sent (after synchronizing the client) it will apply the updates.
+ ///
+ ///
+ internal virtual void WriteFieldSynchronization(FastBufferWriter writer)
+ {
+ WriteField(writer);
+ }
+
///
/// Virtual implementation
///
diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs
index 18ac6a7..613c216 100644
--- a/Runtime/SceneManagement/SceneEventData.cs
+++ b/Runtime/SceneManagement/SceneEventData.cs
@@ -320,9 +320,11 @@ namespace Unity.Netcode
internal void AddSpawnedNetworkObjects()
{
m_NetworkObjectsSync.Clear();
+ // If distributed authority mode and sending to the service, then ignore observers
+ var distributedAuthoritySendingToService = m_NetworkManager.DistributedAuthorityMode && TargetClientId == NetworkManager.ServerClientId;
foreach (var sobj in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
- if (sobj.Observers.Contains(TargetClientId))
+ if (sobj.Observers.Contains(TargetClientId) || distributedAuthoritySendingToService)
{
m_NetworkObjectsSync.Add(sobj);
}
@@ -666,12 +668,14 @@ namespace Unity.Netcode
// Write our count place holder (must not be packed!)
writer.WriteValueSafe((ushort)0);
var distributedAuthority = m_NetworkManager.DistributedAuthorityMode;
+ // If distributed authority mode and sending to the service, then ignore observers
+ var distributedAuthoritySendingToService = distributedAuthority && TargetClientId == NetworkManager.ServerClientId;
foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects)
{
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
{
- if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId))
+ if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId) || distributedAuthoritySendingToService)
{
// Serialize the NetworkObject
var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId, distributedAuthority);
diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs
index d3b2f9a..c17e056 100644
--- a/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/Runtime/Spawning/NetworkSpawnManager.cs
@@ -72,11 +72,22 @@ namespace Unity.Netcode
return;
}
}
+
foreach (var player in m_PlayerObjects)
{
- player.Observers.Add(playerObject.OwnerClientId);
- playerObject.Observers.Add(player.OwnerClientId);
+ // If the player's SpawnWithObservers is not set then do not add the new player object's owner as an observer.
+ if (player.SpawnWithObservers)
+ {
+ player.Observers.Add(playerObject.OwnerClientId);
+ }
+
+ // If the new player object's SpawnWithObservers is not set then do not add this player as an observer to the new player object.
+ if (playerObject.SpawnWithObservers)
+ {
+ playerObject.Observers.Add(player.OwnerClientId);
+ }
}
+
m_PlayerObjects.Add(playerObject);
if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId))
{
@@ -423,8 +434,31 @@ namespace Unity.Netcode
ChangeOwnership(networkObject, NetworkManager.ServerClientId, true);
}
+ private Dictionary m_LastChangeInOwnership = new Dictionary();
+ private const int k_MaximumTickOwnershipChangeMultiplier = 6;
+
internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool isAuthorized, bool isRequestApproval = false)
{
+ // For client-server:
+ // If ownership changes faster than the latency between the client-server and there are NetworkVariables being updated during ownership changes,
+ // then notify the user they could potentially lose state updates if developer logging is enabled.
+ if (!NetworkManager.DistributedAuthorityMode && m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId) && m_LastChangeInOwnership[networkObject.NetworkObjectId] > Time.realtimeSinceStartup)
+ {
+ var hasNetworkVariables = false;
+ for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++)
+ {
+ hasNetworkVariables = networkObject.ChildNetworkBehaviours[i].NetworkVariableFields.Count > 0;
+ if (hasNetworkVariables)
+ {
+ break;
+ }
+ }
+ if (hasNetworkVariables && NetworkManager.LogLevel == LogLevel.Developer)
+ {
+ NetworkLog.LogWarningServer($"[Rapid Ownership Change Detected][Potential Loss in State] Detected a rapid change in ownership that exceeds a frequency less than {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate! Provide at least {k_MaximumTickOwnershipChangeMultiplier}x the current network tick rate between ownership changes to avoid NetworkVariable state loss.");
+ }
+ }
+
if (NetworkManager.DistributedAuthorityMode)
{
// If are not authorized and this is not an approved ownership change, then check to see if we can change ownership
@@ -497,15 +531,21 @@ namespace Unity.Netcode
// Always notify locally on the server when ownership is lost
networkObject.InvokeBehaviourOnLostOwnership();
- networkObject.MarkVariablesDirty(true);
- NetworkManager.BehaviourUpdater.AddForUpdate(networkObject);
-
// Authority adds entries for all client ownership
UpdateOwnershipTable(networkObject, networkObject.OwnerClientId);
// Always notify locally on the server when a new owner is assigned
networkObject.InvokeBehaviourOnGainedOwnership();
+ if (networkObject.PreviousOwnerId == NetworkManager.LocalClientId)
+ {
+ // Mark any owner read variables as dirty
+ networkObject.MarkOwnerReadVariablesDirty();
+ // Immediately queue any pending deltas and order the message before the
+ // change in ownership message.
+ NetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
+ }
+
var size = 0;
if (NetworkManager.DistributedAuthorityMode)
{
@@ -569,6 +609,17 @@ namespace Unity.Netcode
/// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership
/// change can be sent from NetworkBehaviours that override the
networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId);
+
+ // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate.
+ if (!NetworkManager.DistributedAuthorityMode)
+ {
+ if (!m_LastChangeInOwnership.ContainsKey(networkObject.NetworkObjectId))
+ {
+ m_LastChangeInOwnership.Add(networkObject.NetworkObjectId, 0.0f);
+ }
+ var tickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate;
+ m_LastChangeInOwnership[networkObject.NetworkObjectId] = Time.realtimeSinceStartup + (tickFrequency * k_MaximumTickOwnershipChangeMultiplier);
+ }
}
internal bool HasPrefab(NetworkObject.SceneObject sceneObject)
@@ -695,14 +746,14 @@ namespace Unity.Netcode
/// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the
/// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned.
///
- internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3 position = default, Quaternion rotation = default, bool isScenePlaced = false)
+ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false)
{
NetworkObject networkObject = null;
// If the prefab hash has a registered INetworkPrefabInstanceHandler derived class
if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash))
{
// Let the handler spawn the NetworkObject
- networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position, rotation);
+ networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default);
networkObject.NetworkManagerOwner = NetworkManager;
}
else
@@ -752,8 +803,10 @@ namespace Unity.Netcode
}
else
{
- // Create prefab instance
+ // Create prefab instance while applying any pre-assigned position and rotation values
networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent();
+ networkObject.transform.position = position ?? networkObject.transform.position;
+ networkObject.transform.rotation = rotation ?? networkObject.transform.rotation;
networkObject.NetworkManagerOwner = NetworkManager;
networkObject.PrefabGlobalObjectIdHash = globalObjectIdHash;
}
diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs
index 70ed784..a84a7cc 100644
--- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs
+++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs
@@ -24,10 +24,11 @@ namespace Unity.Netcode.TestHelpers.Runtime
/// Used to determine if a NetcodeIntegrationTest is currently running to
/// determine how clients will load scenes
///
+ protected const float k_DefaultTimeoutPeriod = 8.0f;
+ protected const float k_TickFrequency = 1.0f / k_DefaultTickRate;
internal static bool IsRunning { get; private set; }
-
- protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(8.0f);
- protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(1.0f / k_DefaultTickRate);
+ protected static TimeoutHelper s_GlobalTimeoutHelper = new TimeoutHelper(k_DefaultTimeoutPeriod);
+ protected static WaitForSecondsRealtime s_DefaultWaitForTick = new WaitForSecondsRealtime(k_TickFrequency);
public NetcodeLogAssert NetcodeLogAssert;
public enum SceneManagementState
@@ -544,9 +545,14 @@ namespace Unity.Netcode.TestHelpers.Runtime
private bool AllPlayerObjectClonesSpawned(NetworkManager joinedClient)
{
m_InternalErrorLog.Clear();
+ // If we are not checking for spawned players then exit early with a success
+ if (!ShouldCheckForSpawnedPlayers())
+ {
+ return true;
+ }
+
// Continue to populate the PlayerObjects list until all player object (local and clone) are found
ClientNetworkManagerPostStart(joinedClient);
-
var playerObjectRelative = m_ServerNetworkManager.SpawnManager.PlayerObjects.Where((c) => c.OwnerClientId == joinedClient.LocalClientId).FirstOrDefault();
if (playerObjectRelative == null)
{
diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs
index 6fd311f..efa19cf 100644
--- a/Tests/Runtime/ConnectionApproval.cs
+++ b/Tests/Runtime/ConnectionApproval.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Netcode.RuntimeTests
@@ -12,9 +13,10 @@ namespace Unity.Netcode.RuntimeTests
[TestFixture(PlayerCreation.PrefabHash)]
[TestFixture(PlayerCreation.NoPlayer)]
[TestFixture(PlayerCreation.FailValidation)]
- internal class ConnectionApprovalTests : NetcodeIntegrationTest
+ internal class ConnectionApprovalTests : IntegrationTestWithApproximation
{
private const string k_InvalidToken = "Invalid validation token!";
+
public enum PlayerCreation
{
Prefab,
@@ -24,6 +26,8 @@ namespace Unity.Netcode.RuntimeTests
}
private PlayerCreation m_PlayerCreation;
private bool m_ClientDisconnectReasonValidated;
+ private Vector3 m_ExpectedPosition;
+ private Quaternion m_ExpectedRotation;
private Dictionary m_Validated = new Dictionary();
@@ -43,6 +47,12 @@ namespace Unity.Netcode.RuntimeTests
protected override void OnServerAndClientsCreated()
{
+ if (m_PlayerCreation == PlayerCreation.Prefab || m_PlayerCreation == PlayerCreation.PrefabHash)
+ {
+ m_ExpectedPosition = GetRandomVector3(-10.0f, 10.0f);
+ m_ExpectedRotation = Quaternion.Euler(GetRandomVector3(-359.98f, 359.98f));
+ }
+
m_ClientDisconnectReasonValidated = false;
m_BypassConnectionTimeout = m_PlayerCreation == PlayerCreation.FailValidation;
m_Validated.Clear();
@@ -104,11 +114,36 @@ namespace Unity.Netcode.RuntimeTests
return true;
}
+ private bool ValidatePlayersPositionRotation()
+ {
+ foreach (var playerEntries in m_PlayerNetworkObjects)
+ {
+ foreach (var player in playerEntries.Value)
+ {
+ if (!Approximately(player.Value.transform.position, m_ExpectedPosition))
+ {
+ return false;
+ }
+ if (!Approximately(player.Value.transform.rotation, m_ExpectedRotation))
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
[UnityTest]
public IEnumerator ConnectionApproval()
{
yield return WaitForConditionOrTimeOut(ClientAndHostValidated);
AssertOnTimeout("Timed out waiting for all clients to be approved!");
+
+ if (m_PlayerCreation == PlayerCreation.Prefab || m_PlayerCreation == PlayerCreation.PrefabHash)
+ {
+ yield return WaitForConditionOrTimeOut(ValidatePlayersPositionRotation);
+ AssertOnTimeout("Not all player prefabs spawned in the correct position and/or rotation!");
+ }
}
private void NetworkManagerObject_ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
@@ -127,8 +162,8 @@ namespace Unity.Netcode.RuntimeTests
}
response.CreatePlayerObject = ShouldCheckForSpawnedPlayers();
- response.Position = null;
- response.Rotation = null;
+ response.Position = m_ExpectedPosition;
+ response.Rotation = m_ExpectedRotation;
response.PlayerPrefabHash = m_PlayerCreation == PlayerCreation.PrefabHash ? m_PlayerPrefab.GetComponent().GlobalObjectIdHash : null;
}
diff --git a/Tests/Runtime/ConnectionApprovalTimeoutTests.cs b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs
index d324bad..db1210f 100644
--- a/Tests/Runtime/ConnectionApprovalTimeoutTests.cs
+++ b/Tests/Runtime/ConnectionApprovalTimeoutTests.cs
@@ -82,7 +82,7 @@ namespace Unity.Netcode.RuntimeTests
public IEnumerator ValidateApprovalTimeout()
{
// Just delay for a second
- yield return new WaitForSeconds(1);
+ yield return new WaitForSeconds(k_TestTimeoutPeriod * 0.25f);
// Verify we haven't received the time out message yet
NetcodeLogAssert.LogWasNotReceived(LogType.Log, m_ExpectedLogMessage);
diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs
index cda0dd6..bc97fd7 100644
--- a/Tests/Runtime/DeferredMessagingTests.cs
+++ b/Tests/Runtime/DeferredMessagingTests.cs
@@ -670,7 +670,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
foreach (var client in m_ClientNetworkManagers)
{
@@ -678,9 +678,9 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsTrue(manager.DeferMessageCalled);
Assert.IsFalse(manager.ProcessTriggersCalled);
- Assert.AreEqual(4, manager.DeferredMessageCountTotal());
- Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn));
- Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId));
+ Assert.AreEqual(3, manager.DeferredMessageCountTotal());
+ Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn));
+ Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId));
Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab));
AddPrefabsToClient(client);
}
@@ -812,7 +812,7 @@ namespace Unity.Netcode.RuntimeTests
serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
- WaitForAllClientsToReceive();
+ WaitForAllClientsToReceive();
// Validate messages are deferred and pending
foreach (var client in m_ClientNetworkManagers)
@@ -821,10 +821,10 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsTrue(manager.DeferMessageCalled);
Assert.IsFalse(manager.ProcessTriggersCalled);
- Assert.AreEqual(5, manager.DeferredMessageCountTotal());
+ Assert.AreEqual(4, manager.DeferredMessageCountTotal());
- Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn));
- Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId));
+ Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnSpawn));
+ Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId));
Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab));
Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredNetworkMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash));
AddPrefabsToClient(client);
diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs
index c4997fe..af00165 100644
--- a/Tests/Runtime/DisconnectTests.cs
+++ b/Tests/Runtime/DisconnectTests.cs
@@ -180,6 +180,9 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsTrue(m_DisconnectedEvent[m_ServerNetworkManager].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the server {nameof(NetworkManager)} disconnect event entry!");
Assert.IsTrue(m_DisconnectedEvent.ContainsKey(m_ClientNetworkManagers[0]), $"Could not find the client {nameof(NetworkManager)} disconnect event entry!");
Assert.IsTrue(m_DisconnectedEvent[m_ClientNetworkManagers[0]].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the client {nameof(NetworkManager)} disconnect event entry!");
+ Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsIds.Count == 1, $"Expected connected client identifiers count to be 1 but it was {m_ServerNetworkManager.ConnectedClientsIds.Count}!");
+ Assert.IsTrue(m_ServerNetworkManager.ConnectedClients.Count == 1, $"Expected connected client identifiers count to be 1 but it was {m_ServerNetworkManager.ConnectedClients.Count}!");
+ Assert.IsTrue(m_ServerNetworkManager.ConnectedClientsList.Count == 1, $"Expected connected client identifiers count to be 1 but it was {m_ServerNetworkManager.ConnectedClientsList.Count}!");
}
if (m_OwnerPersistence == OwnerPersistence.DestroyWithOwner)
diff --git a/Tests/Runtime/HiddenVariableTests.cs b/Tests/Runtime/HiddenVariableTests.cs
index f110ed1..94b44cf 100644
--- a/Tests/Runtime/HiddenVariableTests.cs
+++ b/Tests/Runtime/HiddenVariableTests.cs
@@ -59,13 +59,13 @@ namespace Unity.Netcode.RuntimeTests
public void Changed(int before, int after)
{
- VerboseDebug($"Value changed from {before} to {after} on {NetworkManager.LocalClientId}");
+ VerboseDebug($"[Client-{NetworkManager.LocalClientId}][{name}][MyNetworkVariable] Value changed from {before} to {after}");
ValueOnClient[NetworkManager.LocalClientId] = after;
}
public void ListChanged(NetworkListEvent listEvent)
{
- Debug.Log($"ListEvent received: type {listEvent.Type}, index {listEvent.Index}, value {listEvent.Value}");
- Debug.Assert(ExpectedSize == MyNetworkList.Count);
+ VerboseDebug($"[Client-{NetworkManager.LocalClientId}][{name}][MyNetworkList] ListEvent received: type {listEvent.Type}, index {listEvent.Index}, value {listEvent.Value}");
+ Debug.Assert(ExpectedSize == MyNetworkList.Count, $"[{name}] List change failure! Expected Count: {ExpectedSize} Actual Count:{MyNetworkList.Count}");
}
}
@@ -185,10 +185,13 @@ namespace Unity.Netcode.RuntimeTests
var otherClient = m_ServerNetworkManager.ConnectedClientsList[2];
m_NetSpawnedObject = SpawnObject(m_TestNetworkPrefab, m_ClientNetworkManagers[1]).GetComponent();
- yield return RefreshGameObects(4);
+ yield return RefreshGameObects(NumberOfClients);
// === Check spawn occurred
yield return WaitForSpawnCount(NumberOfClients + 1);
+
+ AssertOnTimeout($"Timed out waiting for all clients to spawn {m_NetSpawnedObject.name}");
+
Debug.Assert(HiddenVariableObject.SpawnCount == NumberOfClients + 1);
VerboseDebug("Objects spawned");
@@ -205,7 +208,6 @@ namespace Unity.Netcode.RuntimeTests
// ==== Hide our object to a different client
HiddenVariableObject.ExpectedSize = 2;
m_NetSpawnedObject.NetworkHide(otherClient.ClientId);
-
currentValueSet = 3;
m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = currentValueSet;
m_NetSpawnedObject.GetComponent().MyNetworkList.Add(currentValueSet);
@@ -222,7 +224,7 @@ namespace Unity.Netcode.RuntimeTests
VerboseDebug("Object spawned");
// ==== We need a refresh for the newly re-spawned object
- yield return RefreshGameObects(4);
+ yield return RefreshGameObects(NumberOfClients);
currentValueSet = 4;
m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = currentValueSet;
diff --git a/Tests/Runtime/NetworkManagerEventsTests.cs b/Tests/Runtime/NetworkManagerEventsTests.cs
index 195e8b3..8b1c993 100644
--- a/Tests/Runtime/NetworkManagerEventsTests.cs
+++ b/Tests/Runtime/NetworkManagerEventsTests.cs
@@ -13,6 +13,60 @@ namespace Unity.Netcode.RuntimeTests
private NetworkManager m_ClientManager;
private NetworkManager m_ServerManager;
+ private NetworkManager m_NetworkManagerInstantiated;
+ private bool m_Instantiated;
+ private bool m_Destroyed;
+
+ ///
+ /// Validates the and event notifications
+ ///
+ [UnityTest]
+ public IEnumerator InstantiatedAndDestroyingNotifications()
+ {
+ NetworkManager.OnInstantiated += NetworkManager_OnInstantiated;
+ NetworkManager.OnDestroying += NetworkManager_OnDestroying;
+ var waitPeriod = new WaitForSeconds(0.01f);
+ var prefab = new GameObject("InstantiateDestroy");
+ var networkManagerPrefab = prefab.AddComponent();
+
+ Assert.IsTrue(m_Instantiated, $"{nameof(NetworkManager)} prefab did not get instantiated event notification!");
+ Assert.IsTrue(m_NetworkManagerInstantiated == networkManagerPrefab, $"{nameof(NetworkManager)} prefab parameter did not match!");
+
+ m_Instantiated = false;
+ m_NetworkManagerInstantiated = null;
+
+ for (int i = 0; i < 3; i++)
+ {
+ var instance = Object.Instantiate(prefab);
+ var networkManager = instance.GetComponent();
+ Assert.IsTrue(m_Instantiated, $"{nameof(NetworkManager)} instance-{i} did not get instantiated event notification!");
+ Assert.IsTrue(m_NetworkManagerInstantiated == networkManager, $"{nameof(NetworkManager)} instance-{i} parameter did not match!");
+ Object.DestroyImmediate(instance);
+ Assert.IsTrue(m_Destroyed, $"{nameof(NetworkManager)} instance-{i} did not get destroying event notification!");
+ m_Instantiated = false;
+ m_NetworkManagerInstantiated = null;
+ m_Destroyed = false;
+ }
+ m_NetworkManagerInstantiated = networkManagerPrefab;
+ Object.Destroy(prefab);
+ yield return null;
+ Assert.IsTrue(m_Destroyed, $"{nameof(NetworkManager)} prefab did not get destroying event notification!");
+ NetworkManager.OnInstantiated -= NetworkManager_OnInstantiated;
+ NetworkManager.OnDestroying -= NetworkManager_OnDestroying;
+ }
+
+ private void NetworkManager_OnInstantiated(NetworkManager networkManager)
+ {
+ m_Instantiated = true;
+ m_NetworkManagerInstantiated = networkManager;
+ }
+
+ private void NetworkManager_OnDestroying(NetworkManager networkManager)
+ {
+ m_Destroyed = true;
+ Assert.True(m_NetworkManagerInstantiated == networkManager, $"Destroying {nameof(NetworkManager)} and current instance is not a match for the one passed into the event!");
+ }
+
[UnityTest]
public IEnumerator OnServerStoppedCalledWhenServerStops()
{
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs
index faf4402..bda9d15 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs
@@ -265,6 +265,8 @@ namespace Unity.Netcode.RuntimeTests
// After the 1st client has been given ownership to the object, this will be used to make sure each previous owner properly received the remove ownership message
var previousClientComponent = (NetworkObjectOwnershipComponent)null;
+ var networkManagersDAMode = new List();
+
for (int clientIndex = 0; clientIndex < NumberOfClients; clientIndex++)
{
clientObject = clientObjects[clientIndex];
@@ -322,6 +324,21 @@ namespace Unity.Netcode.RuntimeTests
// In distributed authority mode, the current owner just rolls the ownership back over to the DAHost client (i.e. host mocking CMB Service)
if (m_DistributedAuthority)
{
+ // In distributed authority, we have to clear out the NetworkManager instances as this changes relative to authority.
+ networkManagersDAMode.Clear();
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ if (clientNetworkManager.LocalClientId == clientObject.OwnerClientId)
+ {
+ continue;
+ }
+ networkManagersDAMode.Add(clientNetworkManager);
+ }
+
+ if (!UseCMBService() && clientObject.OwnerClientId != m_ServerNetworkManager.LocalClientId)
+ {
+ networkManagersDAMode.Add(m_ServerNetworkManager);
+ }
clientObject.ChangeOwnership(NetworkManager.ServerClientId);
}
else
@@ -330,7 +347,18 @@ namespace Unity.Netcode.RuntimeTests
}
}
- yield return WaitForConditionOrTimeOut(ownershipMessageHooks);
+ if (m_DistributedAuthority)
+ {
+ // We use an alternate method (other than message hooks) to verify each client received the ownership message since message hooks becomes problematic when you need
+ // to make dynamic changes to your targets.
+ yield return WaitForConditionOrTimeOut(() => OwnershipChangedOnAllTargetedClients(networkManagersDAMode, clientObject.NetworkObjectId, NetworkManager.ServerClientId));
+ }
+ else
+ {
+ yield return WaitForConditionOrTimeOut(ownershipMessageHooks);
+ }
+
+
Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for all clients to receive the {nameof(ChangeOwnershipMessage)} message (back to server).");
Assert.That(serverComponent.OnGainedOwnershipFired);
@@ -351,6 +379,22 @@ namespace Unity.Netcode.RuntimeTests
serverComponent.ResetFlags();
}
+ private bool OwnershipChangedOnAllTargetedClients(List networkManagers, ulong networkObjectId, ulong expectedOwner)
+ {
+ foreach (var networkManager in networkManagers)
+ {
+ if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId))
+ {
+ return false;
+ }
+ if (networkManager.SpawnManager.SpawnedObjects[networkObjectId].OwnerClientId != expectedOwner)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
private const int k_NumberOfSpawnedObjects = 5;
private bool AllClientsHaveCorrectObjectCount()
diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs
index 7c8b137..b80e055 100644
--- a/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs
+++ b/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs
@@ -12,8 +12,6 @@ namespace Unity.Netcode.RuntimeTests
internal class NetworkObjectSpawnManyObjectsTests : NetcodeIntegrationTest
{
protected override int NumberOfClients => 1;
- // "many" in this case means enough to exceed a ushort_max message size written in the header
- // 1500 is not a magic number except that it's big enough to trigger a failure
private const int k_SpawnedObjects = 1500;
private NetworkPrefab m_PrefabToSpawn;
@@ -52,19 +50,23 @@ namespace Unity.Netcode.RuntimeTests
}
[UnityTest]
- // When this test fails it does so without an exception and will wait the default ~6 minutes
- [Timeout(10000)]
public IEnumerator WhenManyObjectsAreSpawnedAtOnce_AllAreReceived()
{
+ var timeStarted = Time.realtimeSinceStartup;
for (int x = 0; x < k_SpawnedObjects; x++)
{
NetworkObject serverObject = Object.Instantiate(m_PrefabToSpawn.Prefab).GetComponent();
serverObject.NetworkManagerOwner = m_ServerNetworkManager;
serverObject.Spawn();
}
+
+ var timeSpawned = Time.realtimeSinceStartup - timeStarted;
+ // Provide plenty of time to spawn all 1500 objects in case the CI VM is running slow
+ var timeoutHelper = new TimeoutHelper(30);
// ensure all objects are replicated
- yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects == k_SpawnedObjects);
- AssertOnTimeout($"Timed out waiting for the client to spawn {k_SpawnedObjects} objects!");
+ yield return WaitForConditionOrTimeOut(() => SpawnObjecTrackingComponent.SpawnedObjects == k_SpawnedObjects, timeoutHelper);
+
+ AssertOnTimeout($"Timed out waiting for the client to spawn {k_SpawnedObjects} objects! Time to spawn: {timeSpawned} | Time to timeout: {timeStarted - Time.realtimeSinceStartup}", timeoutHelper);
}
}
}
diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
index 8f65f34..31105ef 100644
--- a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
+++ b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using NUnit.Framework;
using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
@@ -539,5 +540,279 @@ namespace Unity.Netcode.RuntimeTests
}
}
}
+
+ [TestFixture(HostOrServer.DAHost, NetworkTransform.AuthorityModes.Owner)] // Validate the NetworkTransform owner authoritative mode fix using distributed authority
+ [TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Server)] // Validate we have not impacted NetworkTransform server authoritative mode
+ [TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Owner)] // Validate the NetworkTransform owner authoritative mode fix using client-server
+ internal class NestedNetworkTransformTests : IntegrationTestWithApproximation
+ {
+ private const int k_NestedChildren = 5;
+ protected override int NumberOfClients => 2;
+
+ private GameObject m_SpawnObject;
+
+ private NetworkTransform.AuthorityModes m_AuthorityMode;
+
+ private StringBuilder m_ErrorLog = new StringBuilder();
+
+ private List m_NetworkManagers = new List();
+ private List m_SpawnedObjects = new List();
+
+ public NestedNetworkTransformTests(HostOrServer hostOrServer, NetworkTransform.AuthorityModes authorityMode) : base(hostOrServer)
+ {
+ m_AuthorityMode = authorityMode;
+ }
+
+ ///
+ /// Creates a player prefab with several nested NetworkTransforms
+ ///
+ protected override void OnCreatePlayerPrefab()
+ {
+ var networkTransform = m_PlayerPrefab.AddComponent();
+ networkTransform.AuthorityMode = m_AuthorityMode;
+ var parent = m_PlayerPrefab;
+ // Add several nested NetworkTransforms
+ for (int i = 0; i < k_NestedChildren; i++)
+ {
+ var nestedChild = new GameObject();
+ nestedChild.transform.parent = parent.transform;
+ var nestedNetworkTransform = nestedChild.AddComponent();
+ nestedNetworkTransform.AuthorityMode = m_AuthorityMode;
+ nestedNetworkTransform.InLocalSpace = true;
+ parent = nestedChild;
+ }
+ base.OnCreatePlayerPrefab();
+ }
+
+ private void RandomizeObjectTransformPositions(GameObject gameObject)
+ {
+ var networkObject = gameObject.GetComponent();
+ Assert.True(networkObject.ChildNetworkBehaviours.Count > 0);
+
+ foreach (var networkTransform in networkObject.NetworkTransforms)
+ {
+ networkTransform.gameObject.transform.position = GetRandomVector3(-15.0f, 15.0f);
+ }
+ }
+
+ ///
+ /// Randomizes each player's position when validating distributed authority
+ ///
+ ///
+ private GameObject FetchLocalPlayerPrefabToSpawn()
+ {
+ RandomizeObjectTransformPositions(m_PlayerPrefab);
+ return m_PlayerPrefab;
+ }
+
+ ///
+ /// Randomizes the player position when validating client-server
+ ///
+ ///
+ ///
+ private void ConnectionApprovalHandler(NetworkManager.ConnectionApprovalRequest connectionApprovalRequest, NetworkManager.ConnectionApprovalResponse connectionApprovalResponse)
+ {
+ connectionApprovalResponse.Approved = true;
+ connectionApprovalResponse.CreatePlayerObject = true;
+ RandomizeObjectTransformPositions(m_PlayerPrefab);
+ connectionApprovalResponse.Position = GetRandomVector3(-15.0f, 15.0f);
+ }
+
+ protected override void OnServerAndClientsCreated()
+ {
+ // Create a prefab to spawn with each NetworkManager as the owner
+ m_SpawnObject = CreateNetworkObjectPrefab("SpawnObj");
+ var networkTransform = m_SpawnObject.AddComponent();
+ networkTransform.AuthorityMode = m_AuthorityMode;
+ var parent = m_SpawnObject;
+ // Add several nested NetworkTransforms
+ for (int i = 0; i < k_NestedChildren; i++)
+ {
+ var nestedChild = new GameObject();
+ nestedChild.transform.parent = parent.transform;
+ var nestedNetworkTransform = nestedChild.AddComponent();
+ nestedNetworkTransform.AuthorityMode = m_AuthorityMode;
+ nestedNetworkTransform.InLocalSpace = true;
+ parent = nestedChild;
+ }
+
+ if (m_DistributedAuthority)
+ {
+ if (!UseCMBService())
+ {
+ m_ServerNetworkManager.OnFetchLocalPlayerPrefabToSpawn = FetchLocalPlayerPrefabToSpawn;
+ }
+
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ client.OnFetchLocalPlayerPrefabToSpawn = FetchLocalPlayerPrefabToSpawn;
+ }
+ }
+ else
+ {
+ m_ServerNetworkManager.NetworkConfig.ConnectionApproval = true;
+ m_ServerNetworkManager.ConnectionApprovalCallback += ConnectionApprovalHandler;
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ client.NetworkConfig.ConnectionApproval = true;
+ }
+ }
+
+ base.OnServerAndClientsCreated();
+ }
+
+ ///
+ /// Validates the transform positions of two NetworkObject instances
+ ///
+ /// the local instance (source of truth)
+ /// the remote instance
+ ///
+ private bool ValidateTransforms(NetworkObject current, NetworkObject testing)
+ {
+ if (current.ChildNetworkBehaviours.Count == 0 || testing.ChildNetworkBehaviours.Count == 0)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < current.NetworkTransforms.Count - 1; i++)
+ {
+ var transformA = current.NetworkTransforms[i].transform;
+ var transformB = testing.NetworkTransforms[i].transform;
+ if (!Approximately(transformA.position, transformB.position))
+ {
+ m_ErrorLog.AppendLine($"TransformA Position {transformA.position} != TransformB Position {transformB.position}");
+ return false;
+ }
+ if (!Approximately(transformA.localPosition, transformB.localPosition))
+ {
+ m_ErrorLog.AppendLine($"TransformA Local Position {transformA.position} != TransformB Local Position {transformB.position}");
+ return false;
+ }
+ if (transformA.parent != null)
+ {
+ if (current.NetworkTransforms[i].InLocalSpace != testing.NetworkTransforms[i].InLocalSpace)
+ {
+ m_ErrorLog.AppendLine($"NetworkTransform-{current.OwnerClientId}-{current.NetworkTransforms[i].NetworkBehaviourId} InLocalSpace ({current.NetworkTransforms[i].InLocalSpace}) is different from the remote instance version on Client-{testing.NetworkManager.LocalClientId}!");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Validates all player instances spawned with the correct positions including all nested NetworkTransforms
+ /// When running in server authority mode we are validating this fix did not impact that.
+ ///
+ private bool AllClientInstancesSynchronized()
+ {
+ m_ErrorLog.Clear();
+
+ foreach (var current in m_NetworkManagers)
+ {
+ var currentPlayer = current.LocalClient.PlayerObject;
+ var currentNetworkObjectId = currentPlayer.NetworkObjectId;
+ foreach (var testing in m_NetworkManagers)
+ {
+ if (currentPlayer == testing.LocalClient.PlayerObject)
+ {
+ continue;
+ }
+
+ if (!testing.SpawnManager.SpawnedObjects.ContainsKey(currentNetworkObjectId))
+ {
+ m_ErrorLog.AppendLine($"Failed to find Client-{currentPlayer.OwnerClientId}'s player instance on Client-{testing.LocalClientId}!");
+ return false;
+ }
+
+ var remoteInstance = testing.SpawnManager.SpawnedObjects[currentNetworkObjectId];
+ if (!ValidateTransforms(currentPlayer, remoteInstance))
+ {
+ m_ErrorLog.AppendLine($"Failed to validate Client-{currentPlayer.OwnerClientId} against its remote instance on Client-{testing.LocalClientId}!");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Validates that dynamically spawning works the same.
+ /// When running in server authority mode we are validating this fix did not impact that.
+ ///
+ ///
+ private bool AllSpawnedObjectsSynchronized()
+ {
+ m_ErrorLog.Clear();
+
+ foreach (var current in m_SpawnedObjects)
+ {
+ var currentNetworkObject = current.GetComponent();
+ var currentNetworkObjectId = currentNetworkObject.NetworkObjectId;
+ foreach (var testing in m_NetworkManagers)
+ {
+ if (currentNetworkObject.OwnerClientId == testing.LocalClientId)
+ {
+ continue;
+ }
+
+ if (!testing.SpawnManager.SpawnedObjects.ContainsKey(currentNetworkObjectId))
+ {
+ m_ErrorLog.AppendLine($"Failed to find Client-{currentNetworkObject.OwnerClientId}'s player instance on Client-{testing.LocalClientId}!");
+ return false;
+ }
+
+ var remoteInstance = testing.SpawnManager.SpawnedObjects[currentNetworkObjectId];
+ if (!ValidateTransforms(currentNetworkObject, remoteInstance))
+ {
+ m_ErrorLog.AppendLine($"Failed to validate Client-{currentNetworkObject.OwnerClientId} against its remote instance on Client-{testing.LocalClientId}!");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Validates that spawning player and dynamically spawned prefab instances with nested NetworkTransforms
+ /// synchronizes properly in both client-server and distributed authority when using owner authoritative mode.
+ ///
+ [UnityTest]
+ public IEnumerator NestedNetworkTransformSpawnPositionTest()
+ {
+ if (!m_DistributedAuthority || (m_DistributedAuthority && !UseCMBService()))
+ {
+ m_NetworkManagers.Add(m_ServerNetworkManager);
+ }
+ m_NetworkManagers.AddRange(m_ClientNetworkManagers);
+
+ yield return WaitForConditionOrTimeOut(AllClientInstancesSynchronized);
+ AssertOnTimeout($"Failed to synchronize all client instances!\n{m_ErrorLog}");
+
+ foreach (var networkManager in m_NetworkManagers)
+ {
+ // Randomize the position
+ RandomizeObjectTransformPositions(m_SpawnObject);
+
+ // Create an instance owned by the specified networkmanager
+ m_SpawnedObjects.Add(SpawnObject(m_SpawnObject, networkManager));
+ }
+ // Randomize the position once more just to assure we are instantiating remote instances
+ // with a completely different position
+ RandomizeObjectTransformPositions(m_SpawnObject);
+ yield return WaitForConditionOrTimeOut(AllSpawnedObjectsSynchronized);
+ AssertOnTimeout($"Failed to synchronize all spawned NetworkObject instances!\n{m_ErrorLog}");
+ m_SpawnedObjects.Clear();
+ m_NetworkManagers.Clear();
+ }
+
+ protected override IEnumerator OnTearDown()
+ {
+ // In case there was a failure, go ahead and clear these lists out for any pending TextFixture passes
+ m_SpawnedObjects.Clear();
+ m_NetworkManagers.Clear();
+ return base.OnTearDown();
+ }
+ }
}
#endif
diff --git a/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs b/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs
index 7d7e785..0c8a2ff 100644
--- a/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs
+++ b/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs
@@ -5,12 +5,14 @@ using System.Linq;
using System.Text;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
using UnityEngine.TestTools;
using Random = UnityEngine.Random;
namespace Unity.Netcode.RuntimeTests
{
///
+ /// Client-Server only test
/// Validates using managed collections with NetworkVariable.
/// Managed Collections Tested:
/// - List
@@ -18,24 +20,23 @@ namespace Unity.Netcode.RuntimeTests
/// - HashSet
/// This also does some testing on nested collections, but does
/// not test every possible combination.
- ///
- [TestFixture(HostOrServer.Host, CollectionTypes.List)]
- [TestFixture(HostOrServer.Server, CollectionTypes.List)]
+ ///
+ [TestFixture(HostOrServer.Host)]
+ [TestFixture(HostOrServer.Server)]
public class NetworkVariableCollectionsTests : NetcodeIntegrationTest
{
- public enum CollectionTypes
- {
- Dictionary,
- List,
- }
-
protected override int NumberOfClients => 2;
- private CollectionTypes m_CollectionType;
+ private bool m_EnableDebug;
- public NetworkVariableCollectionsTests(HostOrServer hostOrServer, CollectionTypes collectionType) : base(hostOrServer)
+ public NetworkVariableCollectionsTests(HostOrServer hostOrServer) : base(hostOrServer)
{
- m_CollectionType = collectionType;
+ m_EnableDebug = false;
+ }
+
+ protected override bool OnSetVerboseDebug()
+ {
+ return m_EnableDebug;
}
protected override IEnumerator OnSetup()
@@ -50,15 +51,21 @@ namespace Unity.Netcode.RuntimeTests
return base.OnSetup();
}
+ private void AddPlayerComponent() where T : ListTestHelperBase
+ {
+ var component = m_PlayerPrefab.AddComponent();
+ component.SetDebugMode(m_EnableDebug);
+ }
+
protected override void OnCreatePlayerPrefab()
{
- m_PlayerPrefab.AddComponent();
- m_PlayerPrefab.AddComponent();
- m_PlayerPrefab.AddComponent();
- m_PlayerPrefab.AddComponent();
- m_PlayerPrefab.AddComponent();
- m_PlayerPrefab.AddComponent();
- m_PlayerPrefab.AddComponent();
+ AddPlayerComponent();
+ AddPlayerComponent();
+ AddPlayerComponent();
+ AddPlayerComponent();
+ AddPlayerComponent();
+ AddPlayerComponent();
+ AddPlayerComponent();
base.OnCreatePlayerPrefab();
}
@@ -90,6 +97,7 @@ namespace Unity.Netcode.RuntimeTests
{
///////////////////////////////////////////////////////////////////////////
// List Single dimension list
+
compInt = client.LocalClient.PlayerObject.GetComponent();
compIntServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent();
yield return WaitForConditionOrTimeOut(() => compInt.ValidateInstances());
@@ -99,16 +107,34 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"[Server] Not all instances of client-{compIntServer.OwnerClientId}'s {nameof(ListTestHelperInt)} {compIntServer.name} component match!");
var randomInt = Random.Range(int.MinValue, int.MaxValue);
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ //////////////////////////////////
+ // No Write Owner Add Int
+ compIntServer.Add(randomInt, ListTestHelperBase.Targets.Owner);
+ }
+
//////////////////////////////////
// Owner Add int
compInt.Add(randomInt, ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!");
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ //////////////////////////////////
+ // No Write Server Add Int
+ compInt.Add(randomInt, ListTestHelperBase.Targets.Server);
+ }
+
//////////////////////////////////
// Server Add int
compIntServer.Add(randomInt, ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server add failed to synchronize on {nameof(ListTestHelperInt)} {compIntServer.name}!");
+
//////////////////////////////////
// Owner Remove int
var index = Random.Range(0, compInt.ListCollectionOwner.Value.Count - 1);
@@ -131,12 +157,39 @@ namespace Unity.Netcode.RuntimeTests
////////////////////////////////////
// Owner Change int
var valueIntChange = Random.Range(int.MinValue, int.MaxValue);
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // No Write Server Change int with IsDirty restore
+ compIntServer.ListCollectionOwner.Value[index] = valueIntChange;
+ compIntServer.ListCollectionOwner.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Server change failed to restore on {nameof(ListTestHelperInt)} {compInt.name}!");
+
+ // No Write Server Change int with owner state update override
+ compIntServer.ListCollectionOwner.Value[index] = valueIntChange;
+ }
compInt.ListCollectionOwner.Value[index] = valueIntChange;
compInt.ListCollectionOwner.CheckDirtyState();
yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {nameof(ListTestHelperInt)} {compInt.name}!");
+
//////////////////////////////////
// Server Change int
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // No Write Client Change int with IsDirty restore
+ compInt.ListCollectionServer.Value[index] = valueIntChange;
+ compInt.ListCollectionServer.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Client-{client.LocalClientId} change failed to restore on {nameof(ListTestHelperInt)} {compInt.name}!");
+
+ // No Write Client Change int with owner state update override
+ compInt.ListCollectionServer.Value[index] = valueIntChange;
+ }
compIntServer.ListCollectionServer.Value[index] = valueIntChange;
compIntServer.ListCollectionServer.CheckDirtyState();
yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
@@ -211,13 +264,36 @@ namespace Unity.Netcode.RuntimeTests
//////////////////////////////////
// Owner Remove List item
index = Random.Range(0, compListInt.ListCollectionOwner.Value.Count - 1);
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ compListIntServer.ListCollectionOwner.Value.Remove(compListIntServer.ListCollectionOwner.Value[index]);
+ compListIntServer.ListCollectionOwner.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Server remove failed to restore on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}");
+ // No Write Server Remove List item with update restore
+ compListIntServer.ListCollectionOwner.Value.Remove(compListIntServer.ListCollectionOwner.Value[index]);
+ }
compListInt.Remove(compListInt.ListCollectionOwner.Value[index], ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compInt.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {nameof(ListTestHelperListInt)} {compListInt.name}! {compListInt.GetLog()}");
+
//////////////////////////////////
// Server Remove List item
index = Random.Range(0, compListIntServer.ListCollectionServer.Value.Count - 1);
- compListIntServer.Remove(compListIntServer.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Owner);
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // No Write Client Remove List item with CheckDirtyState restore
+ compListInt.Remove(compListInt.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Server);
+ yield return WaitForConditionOrTimeOut(() => compListInt.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Client-{client.LocalClientId} remove failed to restore on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}");
+
+ // No Write Client Remove List item with update restore
+ compListInt.Remove(compListInt.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Server);
+ }
+ compListIntServer.Remove(compListIntServer.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compListIntServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server remove failed to synchronize on {nameof(ListTestHelperListInt)} {compListIntServer.name}! {compListIntServer.GetLog()}");
@@ -370,12 +446,37 @@ namespace Unity.Netcode.RuntimeTests
////////////////////////////////////
// Owner Change SerializableObject
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // No Write Server Remove Serializable item with IsDirty restore
+ compObjectServer.ListCollectionOwner.Value[index] = SerializableObject.GetRandomObject();
+ compObjectServer.ListCollectionOwner.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Server change failed to restore on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!");
+
+ // No Write Server Remove Serializable item with owner state update restore
+ compObjectServer.ListCollectionOwner.Value[index] = SerializableObject.GetRandomObject();
+ }
compObject.ListCollectionOwner.Value[index] = SerializableObject.GetRandomObject();
compObject.ListCollectionOwner.CheckDirtyState();
yield return WaitForConditionOrTimeOut(() => compObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {nameof(ListTestHelperSerializableObject)} {compObject.name}!");
//////////////////////////////////
// Server Change SerializableObject
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // No Write Client Remove Serializable item with IsDirty restore
+ compObject.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject();
+ compObject.ListCollectionServer.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Client-{client.LocalClientId} change failed to restore on {nameof(ListTestHelperSerializableObject)} {compObjectServer.name}!");
+
+ // No Write Client Remove Serializable item with owner state update restore
+ compObject.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject();
+ }
compObjectServer.ListCollectionServer.Value[index] = SerializableObject.GetRandomObject();
compObjectServer.ListCollectionServer.CheckDirtyState();
yield return WaitForConditionOrTimeOut(() => compObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
@@ -427,7 +528,7 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"[Server] Not all instances of client-{compObjectServer.OwnerClientId}'s {nameof(ListTestHelperSerializableObject)} {compObjectServer.name} component match!");
///////////////////////////////////////////////////////////////////////////
- // List> Nested List Validation
+ // List> Nested List Validation
compListObject = client.LocalClient.PlayerObject.GetComponent();
compListObjectServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent();
yield return WaitForConditionOrTimeOut(() => compListObject.ValidateInstances());
@@ -437,24 +538,24 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match! {compListObjectServer.GetLog()}");
//////////////////////////////////
- // Owner Add List item
+ // Owner Add List item
compListObject.Add(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}");
//////////////////////////////////
- // Server Add List item
+ // Server Add List item
compListObjectServer.Add(SerializableObject.GetListOfRandomObjects(5), ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server add failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}");
//////////////////////////////////
- // Owner Remove List item
+ // Owner Remove List item
index = Random.Range(0, compListObject.ListCollectionOwner.Value.Count - 1);
compListObject.Remove(compListObject.ListCollectionOwner.Value[index], ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}");
//////////////////////////////////
- // Server Remove List item
+ // Server Remove List item
index = Random.Range(0, compListObjectServer.ListCollectionServer.Value.Count - 1);
compListObjectServer.Remove(compListObjectServer.ListCollectionServer.Value[index], ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
@@ -468,7 +569,7 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match! {compListObjectServer.GetLog()}");
////////////////////////////////////
- // Owner Change List item
+ // Owner Change List item
index = Random.Range(0, compListObject.ListCollectionOwner.Value.Count - 1);
compListObject.ListCollectionOwner.Value[index] = SerializableObject.GetListOfRandomObjects(5);
compListObject.ListCollectionOwner.CheckDirtyState();
@@ -477,7 +578,7 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"Client-{client.LocalClientId} change index ({index}) failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}");
//////////////////////////////////
- // Server Change List item
+ // Server Change List item
index = Random.Range(0, compListObjectServer.ListCollectionServer.Value.Count - 1);
compListObjectServer.ListCollectionServer.Value[index] = SerializableObject.GetListOfRandomObjects(5);
compListObjectServer.ListCollectionServer.CheckDirtyState();
@@ -486,12 +587,12 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"Server change failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}");
////////////////////////////////////
- // Owner Add Range of List items
+ // Owner Add Range of List items
compListObject.AddRange(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} add range failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}! {compListObject.GetLog()}");
//////////////////////////////////
- // Server Add Range of List items
+ // Server Add Range of List items
compListObjectServer.AddRange(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server add range failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}! {compListObjectServer.GetLog()}");
@@ -503,23 +604,46 @@ namespace Unity.Netcode.RuntimeTests
AssertOnTimeout($"[Server] Not all instances of client-{compListObjectServer.OwnerClientId}'s {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name} component match!");
////////////////////////////////////
- // Owner Full Set List>
+ // Owner Full Set List>
compListObject.FullSet(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} full set failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!");
//////////////////////////////////
- // Server Full Set List>
+ // Server Full Set List>
compListObjectServer.FullSet(SerializableObject.GetListOfListOfRandomObjects(5, 5), ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server full set failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}!");
////////////////////////////////////
- // Owner Clear List>
+ // Owner Clear List>
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // Server Clear List> with IsDirty restore
+ compListObjectServer.ListCollectionOwner.Value.Clear();
+ compListObjectServer.ListCollectionOwner.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Server clear owner collection failed to restore back to last known valid state on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!");
+ // Server Clear List> with update state restore
+ compListObjectServer.ListCollectionOwner.Value.Clear();
+ }
compListObject.Clear(ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!");
//////////////////////////////////
- // Server Clear List>
+ // Server Clear List>
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // Client Clear List> with IsDirty restore
+ compListObject.ListCollectionServer.Value.Clear();
+ compListObject.ListCollectionServer.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compListObject.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Client clear owner collection failed to restore back to last known valid state on {nameof(ListTestHelperListSerializableObject)} {compListObject.name}!");
+
+ // Client Clear List> with update state restore
+ compListObject.ListCollectionServer.Value.Clear();
+ }
compListObjectServer.Clear(ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compListObjectServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server clear failed to synchronize on {nameof(ListTestHelperListSerializableObject)} {compListObjectServer.name}!");
@@ -539,6 +663,111 @@ namespace Unity.Netcode.RuntimeTests
return m_CurrentKey;
}
+ private int m_Stage;
+
+ private List m_Clients;
+
+ private bool m_IsInitialized = false;
+ private StringBuilder m_InitializedStatus = new StringBuilder();
+
+ private IEnumerator ValidateClients(NetworkManager clientBeingTested, bool initialize = false)
+ {
+ VerboseDebug($">>>>>>>>>>>>>>>>>>>>>>>>>[Client-{clientBeingTested.LocalClientId}][{m_Stage}][Validation]<<<<<<<<<<<<<<<<<<<<<<<<< ");
+ m_Stage++;
+ var compDictionary = (DictionaryTestHelper)null;
+ var compDictionaryServer = (DictionaryTestHelper)null;
+ var className = $"{nameof(DictionaryTestHelper)}";
+ var clientsInitialized = new Dictionary();
+
+ var validateTimeout = new TimeoutHelper(0.25f);
+
+ foreach (var client in m_Clients)
+ {
+ var ownerInitialized = false;
+ var serverInitialized = false;
+ ///////////////////////////////////////////////////////////////////////////
+ // Dictionary> nested dictionaries
+ compDictionary = client.LocalClient.PlayerObject.GetComponent();
+ compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent();
+ yield return WaitForConditionOrTimeOut(() => compDictionary.ValidateInstances(), validateTimeout);
+ if (initialize)
+ {
+ if (validateTimeout.HasTimedOut())
+ {
+ m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Owner] Failed validation: {compDictionary.GetLog()}");
+ }
+ else
+ {
+ m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Owner] Passed validation!");
+ }
+ ownerInitialized = !validateTimeout.HasTimedOut();
+ }
+ else
+ {
+ AssertOnTimeout($"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}");
+ }
+
+ yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances(), validateTimeout);
+ if (initialize)
+ {
+ if (validateTimeout.HasTimedOut())
+ {
+ m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Server] Failed validation: {compDictionaryServer.GetLog()}");
+ }
+ else
+ {
+ m_InitializedStatus.AppendLine($"[Client -{client.LocalClientId}][Server] Passed validation!");
+ }
+ serverInitialized = !validateTimeout.HasTimedOut();
+ }
+ else
+ {
+ AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
+ }
+
+ if (initialize)
+ {
+ clientsInitialized.Add(client.LocalClientId, ownerInitialized & serverInitialized);
+ }
+ }
+
+ if (initialize)
+ {
+ m_IsInitialized = true;
+ foreach (var entry in clientsInitialized)
+ {
+ if (!entry.Value)
+ {
+ m_IsInitialized = false;
+ break;
+ }
+ }
+ }
+ }
+
+ private void ValidateClientsFlat(NetworkManager clientBeingTested)
+ {
+ if (!m_EnableDebug)
+ {
+ return;
+ }
+ VerboseDebug($">>>>>>>>>>>>>>>>>>>>>>>>>[{clientBeingTested.name}][{m_Stage}][Validation]<<<<<<<<<<<<<<<<<<<<<<<<< ");
+ m_Stage++;
+ var compDictionary = (DictionaryTestHelper)null;
+ var compDictionaryServer = (DictionaryTestHelper)null;
+ var className = $"{nameof(DictionaryTestHelper)}";
+ foreach (var client in m_Clients)
+ {
+ ///////////////////////////////////////////////////////////////////////////
+ // Dictionary> nested dictionaries
+ compDictionary = client.LocalClient.PlayerObject.GetComponent();
+ compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent();
+ Assert.True(compDictionary.ValidateInstances(), $"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}");
+ Assert.True(compDictionaryServer.ValidateInstances(), $"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
+ }
+ }
+
+
[UnityTest]
public IEnumerator TestDictionaryCollections()
{
@@ -546,15 +775,47 @@ namespace Unity.Netcode.RuntimeTests
var compDictionaryServer = (DictionaryTestHelper)null;
var className = $"{nameof(DictionaryTestHelper)}";
- var clientList = m_ClientNetworkManagers.ToList();
+ m_Clients = m_ClientNetworkManagers.ToList();
if (m_ServerNetworkManager.IsHost)
{
- clientList.Insert(0, m_ServerNetworkManager);
+ m_Clients.Insert(0, m_ServerNetworkManager);
}
m_CurrentKey = 1000;
- foreach (var client in clientList)
+ if (m_EnableDebug)
+ {
+ VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> Init Values <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
+ foreach (var client in m_Clients)
+ {
+ compDictionary = client.LocalClient.PlayerObject.GetComponent();
+ compDictionary.InitValues();
+ compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent();
+ compDictionaryServer.InitValues();
+ }
+ VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> Init Check <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
+ var count = 0;
+ while (count < 3)
+ {
+ m_InitializedStatus.Clear();
+ foreach (var client in m_Clients)
+ {
+ yield return ValidateClients(client, true);
+ }
+ if (m_IsInitialized)
+ {
+ break;
+ }
+ count++;
+ m_Stage = 0;
+ }
+
+ Assert.IsTrue(m_IsInitialized, $"Not all clients synchronized properly!\n {m_InitializedStatus.ToString()}");
+ VerboseDebug(m_InitializedStatus.ToString());
+ }
+
+ VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BEGIN <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
+ foreach (var client in m_Clients)
{
///////////////////////////////////////////////////////////////////////////
// Dictionary> nested dictionaries
@@ -562,18 +823,55 @@ namespace Unity.Netcode.RuntimeTests
compDictionaryServer = m_PlayerNetworkObjects[NetworkManager.ServerClientId][client.LocalClientId].GetComponent();
yield return WaitForConditionOrTimeOut(() => compDictionary.ValidateInstances());
AssertOnTimeout($"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}");
-
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances());
AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
//////////////////////////////////
// Owner Add SerializableObject Entry
- compDictionary.Add((GetNextKey(), SerializableObject.GetRandomObject()), ListTestHelperBase.Targets.Owner);
+ var newEntry = (GetNextKey(), SerializableObject.GetRandomObject());
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // Server-side add same key and SerializableObject prior to being added to the owner side
+ compDictionaryServer.ListCollectionOwner.Value.Add(newEntry.Item1, newEntry.Item2);
+ // Checking if dirty on server side should revert back to origina known current dictionary state
+ compDictionaryServer.ListCollectionOwner.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Server add to owner write collection property failed to restore on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
+ // Server-side add the same key and SerializableObject to owner write permission (would throw key exists exception too if previous failed)
+ compDictionaryServer.ListCollectionOwner.Value.Add(newEntry.Item1, newEntry.Item2);
+ // Server-side add a completely new key and SerializableObject to to owner write permission property
+ compDictionaryServer.ListCollectionOwner.Value.Add(GetNextKey(), SerializableObject.GetRandomObject());
+ // Both should be overridden by the owner-side update
+
+ }
+ VerboseDebug($"[{compDictionary.name}][Owner] Adding Key: {newEntry.Item1}");
+ // Add key and SerializableObject to owner side
+ compDictionary.Add(newEntry, ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} add failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+
+ ValidateClientsFlat(client);
//////////////////////////////////
// Server Add SerializableObject Entry
- compDictionaryServer.Add((GetNextKey(), SerializableObject.GetRandomObject()), ListTestHelperBase.Targets.Server);
+ newEntry = (GetNextKey(), SerializableObject.GetRandomObject());
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // Client-side add same key and SerializableObject to server write permission property
+ compDictionary.ListCollectionServer.Value.Add(newEntry.Item1, newEntry.Item2);
+ // Checking if dirty on client side should revert back to origina known current dictionary state
+ compDictionary.ListCollectionServer.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Client-{client.LocalClientId} add to server write collection property failed to restore on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+ // Client-side add the same key and SerializableObject to server write permission property (would throw key exists exception too if previous failed)
+ compDictionary.ListCollectionServer.Value.Add(newEntry.Item1, newEntry.Item2);
+ // Client-side add a completely new key and SerializableObject to to server write permission property
+ compDictionary.ListCollectionServer.Value.Add(GetNextKey(), SerializableObject.GetRandomObject());
+ // Both should be overridden by the server-side update
+ }
+ VerboseDebug($"[{compDictionaryServer.name}][Server] Adding Key: {newEntry.Item1}");
+ compDictionaryServer.Add(newEntry, ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server add failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
//////////////////////////////////
@@ -583,10 +881,11 @@ namespace Unity.Netcode.RuntimeTests
compDictionary.Remove(valueInt, ListTestHelperBase.Targets.Owner);
yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
AssertOnTimeout($"Client-{client.LocalClientId} remove failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+
//////////////////////////////////
// Server Remove SerializableObject Entry
- index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1);
- valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index];
+ index = Random.Range(0, compDictionary.ListCollectionServer.Value.Keys.Count - 1);
+ valueInt = compDictionary.ListCollectionServer.Value.Keys.ToList()[index];
compDictionaryServer.Remove(valueInt, ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server remove failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
@@ -597,22 +896,110 @@ namespace Unity.Netcode.RuntimeTests
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances());
AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
+ ValidateClientsFlat(client);
////////////////////////////////////
// Owner Change SerializableObject Entry
- index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1);
- valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index];
- compDictionary.ListCollectionOwner.Value[valueInt] = SerializableObject.GetRandomObject();
- compDictionary.ListCollectionOwner.CheckDirtyState();
- yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
- AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+ var randomObject = SerializableObject.GetRandomObject();
+ if (compDictionary.ListCollectionOwner.Value.Keys.Count != 0)
+ {
+ if (compDictionary.ListCollectionOwner.Value.Keys.Count == 1)
+ {
+ index = 0;
+ valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[0];
+ }
+ else
+ {
+ index = Random.Range(0, compDictionary.ListCollectionOwner.Value.Keys.Count - 1);
+ valueInt = compDictionary.ListCollectionOwner.Value.Keys.ToList()[index];
+ }
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // Server-side update same key value prior to being updated to the owner side
+ compDictionaryServer.ListCollectionOwner.Value[valueInt] = randomObject;
+ // Checking if dirty on server side should revert back to origina known current dictionary state
+ compDictionaryServer.ListCollectionOwner.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Server update collection entry value to local owner write collection property failed to restore on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
+
+ // Server-side update same key but with different value prior to being updated to the owner side
+ compDictionaryServer.ListCollectionOwner.Value[valueInt] = SerializableObject.GetRandomObject();
+ if (compDictionaryServer.ListCollectionOwner.Value.Keys.Count > 1)
+ {
+ // Server-side update different key with different value prior to being updated to the owner side
+ compDictionaryServer.ListCollectionOwner.Value[compDictionaryServer.ListCollectionOwner.Value.Keys.ToList()[(index + 1) % compDictionaryServer.ListCollectionOwner.Value.Keys.Count]] = SerializableObject.GetRandomObject();
+ }
+ // Owner-side update should force restore to current known value before updating to the owner's state update of the original index and SerializableObject
+ }
+
+ compDictionary.ListCollectionOwner.Value[valueInt] = randomObject;
+ compDictionary.ListCollectionOwner.CheckDirtyState();
+ yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Client-{client.LocalClientId} change failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+ }
+
//////////////////////////////////
// Server Change SerializableObject
- index = Random.Range(0, compDictionaryServer.ListCollectionOwner.Value.Keys.Count - 1);
- valueInt = compDictionaryServer.ListCollectionOwner.Value.Keys.ToList()[index];
- compDictionaryServer.ListCollectionServer.Value[valueInt] = SerializableObject.GetRandomObject();
- compDictionaryServer.ListCollectionServer.CheckDirtyState();
+ if (compDictionaryServer.ListCollectionServer.Value.Keys.Count != 0)
+ {
+ if (compDictionaryServer.ListCollectionServer.Value.Keys.Count == 1)
+ {
+ index = 0;
+ valueInt = compDictionaryServer.ListCollectionServer.Value.Keys.ToList()[0];
+ }
+ else
+ {
+ index = Random.Range(0, compDictionaryServer.ListCollectionServer.Value.Keys.Count - 1);
+ valueInt = compDictionaryServer.ListCollectionServer.Value.Keys.ToList()[index];
+ }
+
+ // Only test restore on non-host clients (otherwise a host is both server and client/owner)
+ if (!client.IsServer)
+ {
+ // Owner-side update same key value prior to being updated to the server side
+ compDictionary.ListCollectionServer.Value[valueInt] = randomObject;
+ // Checking if dirty on owner side should revert back to origina known current dictionary state
+ compDictionary.ListCollectionServer.IsDirty();
+ yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Client-{client.LocalClientId} update collection entry value to local server write collection property failed to restore on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+
+ // Owner-side update same key but with different value prior to being updated to the server side
+ compDictionary.ListCollectionServer.Value[valueInt] = SerializableObject.GetRandomObject();
+
+ if (compDictionary.ListCollectionServer.Value.Keys.Count > 1)
+ {
+ // Owner-side update different key with different value prior to being updated to the server side
+ compDictionary.ListCollectionServer.Value[compDictionary.ListCollectionServer.Value.Keys.ToList()[(index + 1) % compDictionary.ListCollectionServer.Value.Keys.Count]] = SerializableObject.GetRandomObject();
+ }
+ // Server-side update should force restore to current known value before updating to the server's state update of the original index and SerializableObject
+ }
+
+ compDictionaryServer.ListCollectionServer.Value[valueInt] = SerializableObject.GetRandomObject();
+ compDictionaryServer.ListCollectionServer.CheckDirtyState();
+ yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
+ AssertOnTimeout($"Server change failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
+ }
+ ValidateClientsFlat(client);
+
+ ////////////////////////////////////
+ // Owner Clear
+ compDictionary.Clear(ListTestHelperBase.Targets.Owner);
+ VerboseDebug($"[{compDictionary.name}] Clearing dictionary..");
+ yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
+ AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}");
+ //////////////////////////////////
+ // Server Clear
+ VerboseDebug($"[{compDictionaryServer.name}] Clearing dictionary..");
+ compDictionaryServer.Clear(ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
- AssertOnTimeout($"Server change failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
+ AssertOnTimeout($"Server clear failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
+
+ yield return WaitForConditionOrTimeOut(() => compDictionary.ValidateInstances());
+ AssertOnTimeout($"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}");
+
+ yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances());
+ AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
////////////////////////////////////
// Owner Full Set Dictionary
@@ -624,23 +1011,11 @@ namespace Unity.Netcode.RuntimeTests
compDictionaryServer.FullSet(DictionaryTestHelper.GetDictionaryValues(), ListTestHelperBase.Targets.Server);
yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
AssertOnTimeout($"Server full set failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
-
- ////////////////////////////////////
- // Owner Clear
- compDictionary.Clear(ListTestHelperBase.Targets.Owner);
- yield return WaitForConditionOrTimeOut(() => compDictionary.CompareTrackedChanges(ListTestHelperBase.Targets.Owner));
- AssertOnTimeout($"Client-{client.LocalClientId} clear failed to synchronize on {className} {compDictionary.name}! {compDictionary.GetLog()}");
- //////////////////////////////////
- // Server Clear
- compDictionaryServer.Clear(ListTestHelperBase.Targets.Server);
- yield return WaitForConditionOrTimeOut(() => compDictionaryServer.CompareTrackedChanges(ListTestHelperBase.Targets.Server));
- AssertOnTimeout($"Server clear failed to synchronize on {className} {compDictionaryServer.name}! {compDictionaryServer.GetLog()}");
-
- yield return WaitForConditionOrTimeOut(() => compDictionary.ValidateInstances());
- AssertOnTimeout($"[Owner] Not all instances of client-{compDictionary.OwnerClientId}'s {className} {compDictionary.name} component match! {compDictionary.GetLog()}");
-
- yield return WaitForConditionOrTimeOut(() => compDictionaryServer.ValidateInstances());
- AssertOnTimeout($"[Server] Not all instances of client-{compDictionaryServer.OwnerClientId}'s {className} {compDictionaryServer.name} component match! {compDictionaryServer.GetLog()}");
+ if (m_EnableDebug)
+ {
+ yield return ValidateClients(client);
+ m_Stage = 0;
+ }
}
}
@@ -837,6 +1212,505 @@ namespace Unity.Netcode.RuntimeTests
}
}
+ [TestFixture(HostOrServer.DAHost, CollectionTypes.List)]
+ [TestFixture(HostOrServer.DAHost, CollectionTypes.Dictionary)]
+ [TestFixture(HostOrServer.Host, CollectionTypes.List)]
+ [TestFixture(HostOrServer.Host, CollectionTypes.Dictionary)]
+ [TestFixture(HostOrServer.Server, CollectionTypes.List)]
+ [TestFixture(HostOrServer.Server, CollectionTypes.Dictionary)]
+ public class NetworkVariableCollectionsChangingTests : NetcodeIntegrationTest
+ {
+ protected override int NumberOfClients => 2;
+ public enum CollectionTypes
+ {
+ Dictionary,
+ List,
+ }
+ private StringBuilder m_ErrorLog = new StringBuilder();
+ private CollectionTypes m_CollectionType;
+ private GameObject m_TestPrefab;
+ private NetworkObject m_Instance;
+
+ public NetworkVariableCollectionsChangingTests(HostOrServer hostOrServer, CollectionTypes collectionType) : base(hostOrServer)
+ {
+ m_CollectionType = collectionType;
+ }
+
+ protected override void OnServerAndClientsCreated()
+ {
+ m_TestPrefab = CreateNetworkObjectPrefab("TestObject");
+ if (m_CollectionType == CollectionTypes.Dictionary)
+ {
+ m_TestPrefab.AddComponent();
+ }
+ else
+ {
+ m_TestPrefab.AddComponent();
+ }
+ if (m_DistributedAuthority)
+ {
+ var networkObject = m_TestPrefab.GetComponent();
+ networkObject.SetOwnershipStatus(NetworkObject.OwnershipStatus.Transferable);
+ }
+ base.OnServerAndClientsCreated();
+ }
+
+ private bool AllInstancesSpawned()
+ {
+ if (!UseCMBService())
+ {
+ if (!m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_Instance.NetworkObjectId))
+ {
+ return false;
+ }
+ }
+
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ if (!client.SpawnManager.SpawnedObjects.ContainsKey(m_Instance.NetworkObjectId))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Dictionary m_NetworkManagers = new Dictionary();
+
+ private bool ValidateAllInstances()
+ {
+ if (!m_NetworkManagers.ContainsKey(m_Instance.OwnerClientId))
+ {
+ return false;
+ }
+
+ if (!m_NetworkManagers[m_Instance.OwnerClientId].SpawnManager.SpawnedObjects.ContainsKey(m_Instance.NetworkObjectId))
+ {
+ return false;
+ }
+
+ var ownerNetworkManager = m_NetworkManagers[m_Instance.OwnerClientId];
+
+ var ownerClientInstance = m_NetworkManagers[m_Instance.OwnerClientId].SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent();
+
+ foreach (var client in m_NetworkManagers)
+ {
+ if (client.Value == ownerNetworkManager)
+ {
+ continue;
+ }
+
+ var otherInstance = client.Value.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent();
+ if (!ownerClientInstance.ValidateAgainst(otherInstance))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool OwnershipChangedOnAllClients(ulong expectedOwner)
+ {
+ m_ErrorLog.Clear();
+ foreach (var client in m_NetworkManagers)
+ {
+ var otherInstance = client.Value.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent();
+ if (otherInstance.OwnerClientId != expectedOwner)
+ {
+ m_ErrorLog.AppendLine($"Client-{client.Value.LocalClientId} instance of {m_Instance.name} still shows the owner is Client-{otherInstance.OwnerClientId} when it should be Client-{expectedOwner}!");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private BaseCollectionUpdateHelper GetOwnerInstance()
+ {
+ var ownerNetworkManager = m_NetworkManagers[m_Instance.OwnerClientId];
+ return m_NetworkManagers[m_Instance.OwnerClientId].SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent();
+ }
+
+ ///
+ /// Gets the authority instance.
+ /// Client-Server: will always return the server-side instance
+ /// Distributed Authority: will always return the owner
+ ///
+ /// authority instance
+ private BaseCollectionUpdateHelper GetAuthorityInstance()
+ {
+ if (m_DistributedAuthority)
+ {
+ return GetOwnerInstance();
+ }
+ else
+ {
+ return m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].GetComponent();
+ }
+ }
+
+ [UnityTest]
+ public IEnumerator CollectionAndOwnershipChangingTest()
+ {
+ BaseCollectionUpdateHelper.VerboseMode = m_EnableVerboseDebug;
+ var runWaitPeriod = new WaitForSeconds(0.5f);
+ m_NetworkManagers.Clear();
+ if (!UseCMBService() && m_UseHost)
+ {
+ m_NetworkManagers.Add(m_ServerNetworkManager.LocalClientId, m_ServerNetworkManager);
+ }
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ m_NetworkManagers.Add(client.LocalClientId, client);
+ }
+
+ var authorityNetworkManager = UseCMBService() || !m_UseHost ? m_ClientNetworkManagers[0] : m_ServerNetworkManager;
+
+ var instance = SpawnObject(m_TestPrefab, authorityNetworkManager);
+ m_Instance = instance.GetComponent();
+ var helper = instance.GetComponent();
+ var currentOwner = helper.OwnerClientId;
+ yield return WaitForConditionOrTimeOut(AllInstancesSpawned);
+ AssertOnTimeout($"[Pre][1st Phase] Timed out waiting for all clients to spawn {m_Instance.name}!");
+ helper.SetState(BaseCollectionUpdateHelper.HelperStates.Start);
+ yield return runWaitPeriod;
+
+ // Update values, validate values, change owner, updates values, and repeat until all clients have been the owner at least once
+ for (int i = 0; i < 4; i++)
+ {
+ helper.SetState(BaseCollectionUpdateHelper.HelperStates.Pause);
+ yield return WaitForConditionOrTimeOut(ValidateAllInstances);
+ AssertOnTimeout($"[1st Phase] Timed out waiting for all clients to validdate their values!");
+ helper.SetState(BaseCollectionUpdateHelper.HelperStates.Start);
+ yield return s_DefaultWaitForTick;
+
+ currentOwner = GetAuthorityInstance().ChangeOwner();
+ Assert.IsFalse(currentOwner == ulong.MaxValue, "A non-authority instance attempted to change ownership!");
+
+ yield return WaitForConditionOrTimeOut(() => OwnershipChangedOnAllClients(currentOwner));
+ AssertOnTimeout($"[1st Phase] Timed out waiting for all clients to change ownership!\n {m_ErrorLog.ToString()}");
+ helper = GetOwnerInstance();
+ yield return runWaitPeriod;
+ }
+
+ // Now reset the values
+ helper.SetState(BaseCollectionUpdateHelper.HelperStates.Pause);
+ helper.Clear();
+
+ // Validate all instances are reset
+ yield return WaitForConditionOrTimeOut(ValidateAllInstances);
+ AssertOnTimeout($"[Pre][2nd Phase]Timed out waiting for all clients to validdate their values!");
+ helper.SetState(BaseCollectionUpdateHelper.HelperStates.Start);
+
+ // Update, change ownership, and repeat until all clients have been the owner at least once
+ for (int i = 0; i < 4; i++)
+ {
+ yield return runWaitPeriod;
+ currentOwner = GetAuthorityInstance().ChangeOwner();
+ Assert.IsFalse(currentOwner == ulong.MaxValue, "A non-authority instance attempted to change ownership!");
+ yield return WaitForConditionOrTimeOut(() => OwnershipChangedOnAllClients(currentOwner));
+ AssertOnTimeout($"[2nd Phase] Timed out waiting for all clients to change ownership!");
+ helper = GetOwnerInstance();
+ }
+
+ helper.SetState(BaseCollectionUpdateHelper.HelperStates.Pause);
+ yield return WaitForConditionOrTimeOut(ValidateAllInstances);
+ AssertOnTimeout($"[Last Validate] Timed out waiting for all clients to validdate their values!");
+ }
+ }
+
+ #region COLLECTION CHANGING COMPONENTS
+ ///
+ /// Helper class to test adding dictionary entries rapidly with frequent ownership changes.
+ /// This includes a companion integer that is continually incremented and used as the key value for each entry.
+ ///
+ public class DictionaryCollectionUpdateHelper : BaseCollectionUpdateHelper
+ {
+ private NetworkVariable> m_DictionaryCollection = new NetworkVariable>(new Dictionary(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
+ private NetworkVariable m_CurrentKeyValue = new NetworkVariable(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
+
+ protected override bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper)
+ {
+ var otherListHelper = otherHelper as DictionaryCollectionUpdateHelper;
+ var localValues = m_DictionaryCollection.Value;
+ var otherValues = otherListHelper.m_DictionaryCollection.Value;
+
+ if (localValues.Count != otherValues.Count)
+ {
+ return false;
+ }
+
+ foreach (var entry in m_DictionaryCollection.Value)
+ {
+ if (!otherValues.ContainsKey(entry.Key))
+ {
+ return false;
+ }
+
+ if (entry.Value != otherValues[entry.Key])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ protected override void OnClear()
+ {
+ m_DictionaryCollection.Value.Clear();
+ m_DictionaryCollection.CheckDirtyState();
+ base.OnClear();
+ }
+
+ protected override void AddItem()
+ {
+ m_DictionaryCollection.Value.Add(m_CurrentKeyValue.Value, m_CurrentKeyValue.Value);
+ m_DictionaryCollection.CheckDirtyState();
+ m_CurrentKeyValue.Value++;
+ }
+ }
+
+ ///
+ /// Helper class to test adding list entries rapidly with frequent ownership changes
+ ///
+ public class ListCollectionUpdateHelper : BaseCollectionUpdateHelper
+ {
+ private NetworkVariable> m_ListCollection = new NetworkVariable>(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
+
+
+ protected override bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper)
+ {
+ var otherListHelper = otherHelper as ListCollectionUpdateHelper;
+ var localValues = m_ListCollection.Value;
+ var otherValues = otherListHelper.m_ListCollection.Value;
+
+ if (localValues.Count != otherValues.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < localValues.Count - 1; i++)
+ {
+ if (localValues[i] != i)
+ {
+ return false;
+ }
+
+ if (localValues[i] != otherValues[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ protected override void OnClear()
+ {
+ m_ListCollection.Value.Clear();
+ m_ListCollection.CheckDirtyState();
+ base.OnClear();
+ }
+
+ protected override void AddItem()
+ {
+ m_ListCollection.Value.Add(m_ListCollection.Value.Count);
+ m_ListCollection.CheckDirtyState();
+ }
+ }
+
+ ///
+ /// The base class to test rapidly adding items to a collection type
+ ///
+ public class BaseCollectionUpdateHelper : NetworkBehaviour
+ {
+ public static bool VerboseMode;
+ private const int k_OwnershipTickDelay = 1;
+
+ public enum HelperStates
+ {
+ Stop,
+ Start,
+ Pause,
+ ClearToChangeOwner,
+ ChangingOwner
+ }
+ public HelperStates HelperState { get; private set; }
+
+ private int m_SendClearForOwnershipOnTick;
+ private ulong m_NextClient = 0;
+ private ulong m_ClientToSendClear = 0;
+
+ public void SetState(HelperStates helperState)
+ {
+ HelperState = helperState;
+ }
+
+ protected virtual bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper)
+ {
+ return true;
+ }
+
+ public bool ValidateAgainst(BaseCollectionUpdateHelper otherHelper)
+ {
+ return OnValidateAgainst(otherHelper);
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ // Register for tick updates
+ NetworkManager.NetworkTickSystem.Tick += OnNetworkTick;
+
+ base.OnNetworkSpawn();
+ }
+ public override void OnNetworkDespawn()
+ {
+ NetworkManager.NetworkTickSystem.Tick -= OnNetworkTick;
+ base.OnNetworkDespawn();
+ }
+
+ protected virtual void OnClear()
+ {
+ }
+
+ public void Clear()
+ {
+ OnClear();
+ }
+
+ protected virtual void AddItem()
+ {
+ }
+
+ private bool CanUpdate()
+ {
+ return HelperState == HelperStates.Start;
+ }
+
+ private void Update()
+ {
+ // Exit early if not spawn, updating is not enabled, or is not the owner
+ if (!IsSpawned || !CanUpdate() || !IsOwner)
+ {
+ return;
+ }
+
+ AddItem();
+ }
+
+ protected override void OnOwnershipChanged(ulong previous, ulong current)
+ {
+ // When the ownership changes and the client is the owner, then immediately add an item to the collection
+ if (NetworkManager.LocalClientId == current)
+ {
+ AddItem();
+ }
+ base.OnOwnershipChanged(previous, current);
+ }
+
+
+ ///
+ /// Sets the tick delay period of time to provide all in-flight deltas to be processed.
+ ///
+ private void SetTickDelay()
+ {
+ m_SendClearForOwnershipOnTick = NetworkManager.ServerTime.Tick + k_OwnershipTickDelay;
+ }
+
+ ///
+ /// Changes the ownership
+ ///
+ /// next owner or ulong.MaxValue that means the authority did not invoke this method
+ public ulong ChangeOwner()
+ {
+ if (HasAuthority && !IsOwnershipChanging())
+ {
+ var index = NetworkManager.ConnectedClientsIds.ToList().IndexOf(OwnerClientId);
+ index++;
+ index = index % NetworkManager.ConnectedClientsIds.Count;
+ m_NextClient = NetworkManager.ConnectedClientsIds[index];
+
+ // If we are in distributed authority and the authority or we are in client-server and the server, then make the change ourselves.
+ if (OwnerClientId == NetworkManager.LocalClientId && (NetworkManager.DistributedAuthorityMode || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer)))
+ {
+ HelperState = HelperStates.ChangingOwner;
+ SetTickDelay();
+ Log($"Locally changing ownership to Client-{m_NextClient}");
+ }
+
+ if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && OwnerClientId != NetworkManager.LocalClientId)
+ {
+ // If we are transitioning between a client to the host or client to client,
+ // send a "heads-up" Rpc to the client prior to changing ownership. The client
+ // will stop updating for the tick delay period and then send a confirmation
+ // to the host that it is clear to change ownership.
+ ChangingOwnershipRpc(RpcTarget.Single(OwnerClientId, RpcTargetUse.Temp));
+ Log($"Remotely changing ownership to Client-{m_NextClient}");
+ }
+
+ return m_NextClient;
+ }
+
+ return ulong.MaxValue;
+ }
+
+ ///
+ /// Sent by the host to a client when ownership is transitioning from a client to
+ /// the host or to another client.
+ ///
+ [Rpc(SendTo.SpecifiedInParams)]
+ private void ChangingOwnershipRpc(RpcParams rpcParams = default)
+ {
+ // The sender is who we respond to that it is clear to change ownership
+ m_ClientToSendClear = rpcParams.Receive.SenderClientId;
+ HelperState = HelperStates.ClearToChangeOwner;
+ SetTickDelay();
+ }
+
+ ///
+ /// Notification that the current owner has stopped updating and ownership
+ /// updates can occur without missed updates.
+ ///
+ ///
+ [Rpc(SendTo.SpecifiedInParams)]
+ private void ChangingOwnershipClearRpc(RpcParams rpcParams = default)
+ {
+ HelperState = HelperStates.ChangingOwner;
+ SetTickDelay();
+ Log($"Changing ownership to Client-{m_NextClient} based on ready request.");
+ }
+
+ private bool IsOwnershipChanging()
+ {
+ return HelperState == HelperStates.ClearToChangeOwner || HelperState == HelperStates.ChangingOwner;
+ }
+
+ private void OnNetworkTick()
+ {
+ if (!IsSpawned || !IsOwnershipChanging() || m_SendClearForOwnershipOnTick > NetworkManager.ServerTime.Tick)
+ {
+ return;
+ }
+
+ if (HelperState == HelperStates.ChangingOwner)
+ {
+ NetworkObject.ChangeOwnership(m_NextClient);
+ Log($"Local Change ownership to Client-{m_NextClient} complete! New Owner is {NetworkObject.OwnerClientId} | Expected {m_NextClient}");
+ }
+ else
+ {
+ ChangingOwnershipClearRpc(RpcTarget.Single(m_ClientToSendClear, RpcTargetUse.Temp));
+ }
+ HelperState = HelperStates.Stop;
+ }
+
+ protected void Log(string msg)
+ {
+ if (VerboseMode)
+ {
+ Debug.Log($"[Client-{NetworkManager.LocalClientId}] {msg}");
+ }
+ }
+ }
+ #endregion
+
#region HASHSET COMPONENT HELPERS
public class HashSetBaseTypeTestHelper : ListTestHelperBase, IHashSetTestHelperBase
{
@@ -1649,6 +2523,14 @@ namespace Unity.Netcode.RuntimeTests
ListCollectionServer.OnValueChanged += OnServerListValuesChanged;
ListCollectionOwner.OnValueChanged += OnOwnerListValuesChanged;
+ if (!IsDebugMode)
+ {
+ InitValues();
+ }
+ }
+
+ public void InitValues()
+ {
if (IsServer)
{
ListCollectionServer.Value = OnSetServerValues();
@@ -1660,8 +2542,8 @@ namespace Unity.Netcode.RuntimeTests
ListCollectionOwner.Value = OnSetOwnerValues();
ListCollectionOwner.CheckDirtyState();
}
- base.OnNetworkPostSpawn();
}
+
public override void OnNetworkDespawn()
{
ListCollectionServer.OnValueChanged -= OnServerListValuesChanged;
@@ -1705,12 +2587,15 @@ namespace Unity.Netcode.RuntimeTests
return list;
}
-
-
public int IntValue;
public long LongValue;
public float FloatValue;
+ public override string ToString()
+ {
+ return $"{IntValue},{LongValue},{FloatValue}";
+ }
+
public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref IntValue);
@@ -2602,7 +3487,6 @@ namespace Unity.Netcode.RuntimeTests
Instances.Clear();
}
-
public NetworkVariable> ListCollectionServer = new NetworkVariable>(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
public NetworkVariable> ListCollectionOwner = new NetworkVariable>(new List(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
// This tracks what has changed per instance which is used to compare to all other instances
@@ -2865,6 +3749,8 @@ namespace Unity.Netcode.RuntimeTests
#region BASE TEST COMPONENT HELPERS
public class ListTestHelperBase : NetworkBehaviour
{
+ protected static bool IsDebugMode { get; private set; }
+
public enum Targets
{
Server,
@@ -2897,6 +3783,10 @@ namespace Unity.Netcode.RuntimeTests
m_StringBuilder.AppendLine($"[Client-{NetworkManager.LocalClientId}][{name}] Log Started.");
}
+ public void SetDebugMode(bool isDebug)
+ {
+ IsDebugMode = isDebug;
+ }
public virtual bool CompareTrackedChanges(Targets target)
{
diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs
index 9ec32bc..601a1ba 100644
--- a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs
+++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs
@@ -1,5 +1,7 @@
#if COM_UNITY_MODULES_PHYSICS
using System.Collections;
+using System.Collections.Generic;
+using System.Text;
using NUnit.Framework;
using Unity.Netcode.Components;
using Unity.Netcode.TestHelpers.Runtime;
@@ -108,5 +110,386 @@ namespace Unity.Netcode.RuntimeTests
Assert.IsTrue(clientPlayerInstance == null, $"[Client-Side] Player {nameof(NetworkObject)} is not null!");
}
}
+
+ internal class ContactEventTransformHelperWithInfo : ContactEventTransformHelper, IContactEventHandlerWithInfo
+ {
+ public ContactEventHandlerInfo GetContactEventHandlerInfo()
+ {
+ var contactEventHandlerInfo = new ContactEventHandlerInfo()
+ {
+ HasContactEventPriority = IsOwner,
+ ProvideNonRigidBodyContactEvents = m_EnableNonRigidbodyContacts.Value,
+ };
+ return contactEventHandlerInfo;
+ }
+
+ protected override void OnRegisterForContactEvents(bool isRegistering)
+ {
+ RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering);
+ }
+ }
+
+
+ internal class ContactEventTransformHelper : NetworkTransform, IContactEventHandler
+ {
+ public static Vector3 SessionOwnerSpawnPoint;
+ public static Vector3 ClientSpawnPoint;
+ public static bool VerboseDebug;
+ public enum HelperStates
+ {
+ None,
+ MoveForward,
+ }
+
+ private HelperStates m_HelperState;
+
+ public void SetHelperState(HelperStates state)
+ {
+ m_HelperState = state;
+ if (!m_NetworkRigidbody.IsKinematic())
+ {
+ m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero;
+ m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero;
+ }
+ m_NetworkRigidbody.Rigidbody.isKinematic = m_HelperState == HelperStates.None;
+ if (!m_NetworkRigidbody.IsKinematic())
+ {
+ m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero;
+ m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero;
+ }
+
+ }
+
+ protected struct ContactEventInfo
+ {
+ public ulong EventId;
+ public Vector3 AveragedCollisionNormal;
+ public Rigidbody CollidingBody;
+ public Vector3 ContactPoint;
+ }
+
+ protected List m_ContactEvents = new List();
+
+ protected NetworkVariable m_EnableNonRigidbodyContacts = new NetworkVariable(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
+
+ protected NetworkRigidbody m_NetworkRigidbody;
+ public ContactEventTransformHelper Target;
+
+ public bool HasContactEvents()
+ {
+ return m_ContactEvents.Count > 0;
+ }
+
+ public Rigidbody GetRigidbody()
+ {
+ return m_NetworkRigidbody.Rigidbody;
+ }
+
+ public bool HadContactWith(ContactEventTransformHelper otherObject)
+ {
+ if (otherObject == null)
+ {
+ return false;
+ }
+ foreach (var contactEvent in m_ContactEvents)
+ {
+ if (contactEvent.CollidingBody == otherObject.m_NetworkRigidbody.Rigidbody)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected virtual void CheckToStopMoving()
+ {
+ SetHelperState(HadContactWith(Target) ? HelperStates.None : HelperStates.MoveForward);
+ }
+
+ public void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default)
+ {
+ if (Target == null)
+ {
+ return;
+ }
+
+ if (collidingBody != null)
+ {
+ Log($">>>>>>> contact event with {collidingBody.name}!");
+ }
+ else
+ {
+ Log($">>>>>>> contact event with non-rigidbody!");
+ }
+
+ m_ContactEvents.Add(new ContactEventInfo()
+ {
+ EventId = eventId,
+ AveragedCollisionNormal = averagedCollisionNormal,
+ CollidingBody = collidingBody,
+ ContactPoint = contactPoint,
+ });
+ CheckToStopMoving();
+ }
+
+ private void SetInitialPositionClientServer()
+ {
+ if (IsServer)
+ {
+ if (!NetworkManager.DistributedAuthorityMode && !IsLocalPlayer)
+ {
+ transform.position = ClientSpawnPoint;
+ m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint;
+ }
+ else
+ {
+ transform.position = SessionOwnerSpawnPoint;
+ m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint;
+ }
+ }
+ else
+ {
+ transform.position = ClientSpawnPoint;
+ m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint;
+ }
+ }
+
+ private void SetInitialPositionDistributedAuthority()
+ {
+ if (HasAuthority)
+ {
+ if (IsSessionOwner)
+ {
+ transform.position = SessionOwnerSpawnPoint;
+ m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint;
+ }
+ else
+ {
+ transform.position = ClientSpawnPoint;
+ m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint;
+ }
+ }
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ m_NetworkRigidbody = GetComponent();
+
+ m_NetworkRigidbody.Rigidbody.maxLinearVelocity = 15;
+ m_NetworkRigidbody.Rigidbody.maxAngularVelocity = 10;
+
+ if (NetworkManager.DistributedAuthorityMode)
+ {
+ SetInitialPositionDistributedAuthority();
+ }
+ else
+ {
+ SetInitialPositionClientServer();
+ }
+ if (IsLocalPlayer)
+ {
+ RegisterForContactEvents(true);
+ }
+ else
+ {
+ m_NetworkRigidbody.Rigidbody.detectCollisions = false;
+ }
+ base.OnNetworkSpawn();
+ }
+
+ protected virtual void OnRegisterForContactEvents(bool isRegistering)
+ {
+ RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering);
+ }
+
+ public void RegisterForContactEvents(bool isRegistering)
+ {
+ OnRegisterForContactEvents(isRegistering);
+ }
+
+ private void FixedUpdate()
+ {
+ if (!IsSpawned || !IsOwner || m_HelperState != HelperStates.MoveForward)
+ {
+ return;
+ }
+ var distance = Vector3.Distance(Target.transform.position, transform.position);
+ var moveAmount = Mathf.Max(1.2f, distance);
+ // Head towards our target
+ var dir = (Target.transform.position - transform.position).normalized;
+ var deltaMove = dir * moveAmount * Time.fixedDeltaTime;
+ m_NetworkRigidbody.Rigidbody.MovePosition(m_NetworkRigidbody.Rigidbody.position + deltaMove);
+
+
+ Log($" Loc: {transform.position} | Dest: {Target.transform.position} | Dist: {distance} | MoveDelta: {deltaMove}");
+ }
+
+ protected void Log(string msg)
+ {
+ if (VerboseDebug)
+ {
+ Debug.Log($"Client-{OwnerClientId} {msg}");
+ }
+ }
+ }
+
+ [TestFixture(HostOrServer.Host, ContactEventTypes.Default)]
+ [TestFixture(HostOrServer.DAHost, ContactEventTypes.Default)]
+ [TestFixture(HostOrServer.Host, ContactEventTypes.WithInfo)]
+ [TestFixture(HostOrServer.DAHost, ContactEventTypes.WithInfo)]
+ internal class RigidbodyContactEventManagerTests : IntegrationTestWithApproximation
+ {
+ protected override int NumberOfClients => 1;
+
+
+ private GameObject m_RigidbodyContactEventManager;
+
+ public enum ContactEventTypes
+ {
+ Default,
+ WithInfo
+ }
+
+ private ContactEventTypes m_ContactEventType;
+ private StringBuilder m_ErrorLogger = new StringBuilder();
+
+ public RigidbodyContactEventManagerTests(HostOrServer hostOrServer, ContactEventTypes contactEventType) : base(hostOrServer)
+ {
+ m_ContactEventType = contactEventType;
+ }
+
+ protected override void OnCreatePlayerPrefab()
+ {
+ ContactEventTransformHelper.SessionOwnerSpawnPoint = GetRandomVector3(-4, -3);
+ ContactEventTransformHelper.ClientSpawnPoint = GetRandomVector3(3, 4);
+ if (m_ContactEventType == ContactEventTypes.Default)
+ {
+ var helper = m_PlayerPrefab.AddComponent();
+ helper.AuthorityMode = NetworkTransform.AuthorityModes.Owner;
+ }
+ else
+ {
+ var helperWithInfo = m_PlayerPrefab.AddComponent();
+ helperWithInfo.AuthorityMode = NetworkTransform.AuthorityModes.Owner;
+ }
+
+ var rigidbody = m_PlayerPrefab.AddComponent();
+ rigidbody.useGravity = false;
+ rigidbody.isKinematic = true;
+ rigidbody.mass = 5.0f;
+ rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
+ var sphereCollider = m_PlayerPrefab.AddComponent();
+ sphereCollider.radius = 0.5f;
+ sphereCollider.providesContacts = true;
+
+ var networkRigidbody = m_PlayerPrefab.AddComponent();
+ networkRigidbody.UseRigidBodyForMotion = true;
+ networkRigidbody.AutoUpdateKinematicState = false;
+
+ m_RigidbodyContactEventManager = new GameObject();
+ m_RigidbodyContactEventManager.AddComponent();
+ }
+
+
+
+ private bool PlayersSpawnedInRightLocation()
+ {
+ var position = m_ServerNetworkManager.LocalClient.PlayerObject.transform.position;
+ if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, position))
+ {
+ m_ErrorLogger.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!");
+ return false;
+ }
+
+ position = m_ClientNetworkManagers[0].LocalClient.PlayerObject.transform.position;
+ if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position))
+ {
+ m_ErrorLogger.AppendLine($"Client-{m_ClientNetworkManagers[0].LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!");
+ return false;
+ }
+ var playerObject = (NetworkObject)null;
+ if (!m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_ClientNetworkManagers[0].LocalClient.PlayerObject.NetworkObjectId))
+ {
+ m_ErrorLogger.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} cannot find a local spawned instance of Client-{m_ClientNetworkManagers[0].LocalClientId}'s player object!");
+ return false;
+ }
+ playerObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_ClientNetworkManagers[0].LocalClient.PlayerObject.NetworkObjectId];
+ position = playerObject.transform.position;
+
+ if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position))
+ {
+ m_ErrorLogger.AppendLine($"Client-{m_ServerNetworkManager.LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!");
+ return false;
+ }
+
+ if (!m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects.ContainsKey(m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId))
+ {
+ m_ErrorLogger.AppendLine($"Client-{m_ClientNetworkManagers[0].LocalClientId} cannot find a local spawned instance of Client-{m_ServerNetworkManager.LocalClientId}'s player object!");
+ return false;
+ }
+ playerObject = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId];
+ position = playerObject.transform.position;
+ if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, playerObject.transform.position))
+ {
+ m_ErrorLogger.AppendLine($"Client-{m_ClientNetworkManagers[0].LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!");
+ return false;
+ }
+ return true;
+ }
+
+
+ [UnityTest]
+ public IEnumerator TestContactEvents()
+ {
+ ContactEventTransformHelper.VerboseDebug = m_EnableVerboseDebug;
+
+ m_PlayerPrefab.SetActive(false);
+ m_ErrorLogger.Clear();
+ // Validate all instances are spawned in the right location
+ yield return WaitForConditionOrTimeOut(PlayersSpawnedInRightLocation);
+ AssertOnTimeout($"Timed out waiting for all player instances to spawn in the corect location:\n {m_ErrorLogger}");
+ m_ErrorLogger.Clear();
+
+ var sessionOwnerPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent() :
+ m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent();
+ var clientPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent() :
+ m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent();
+
+ // Get both players to point towards each other
+ sessionOwnerPlayer.Target = clientPlayer;
+ clientPlayer.Target = sessionOwnerPlayer;
+
+ sessionOwnerPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward);
+ clientPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward);
+
+
+ yield return WaitForConditionOrTimeOut(() => sessionOwnerPlayer.HadContactWith(clientPlayer) || clientPlayer.HadContactWith(sessionOwnerPlayer));
+ AssertOnTimeout("Timed out waiting for a player to collide with another player!");
+
+ clientPlayer.RegisterForContactEvents(false);
+ sessionOwnerPlayer.RegisterForContactEvents(false);
+ var otherPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ServerNetworkManager.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent() :
+ m_ServerNetworkManager.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent();
+ otherPlayer.RegisterForContactEvents(false);
+ otherPlayer = m_ContactEventType == ContactEventTypes.Default ? m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent() :
+ m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent();
+ otherPlayer.RegisterForContactEvents(false);
+
+ Object.Destroy(m_RigidbodyContactEventManager);
+ m_RigidbodyContactEventManager = null;
+ }
+
+ protected override IEnumerator OnTearDown()
+ {
+ // In case of a test failure
+ if (m_RigidbodyContactEventManager)
+ {
+ Object.Destroy(m_RigidbodyContactEventManager);
+ m_RigidbodyContactEventManager = null;
+ }
+
+ return base.OnTearDown();
+ }
+ }
}
#endif // COM_UNITY_MODULES_PHYSICS
diff --git a/Tests/Runtime/PlayerObjectTests.cs b/Tests/Runtime/PlayerObjectTests.cs
index 8fba758..e382bcf 100644
--- a/Tests/Runtime/PlayerObjectTests.cs
+++ b/Tests/Runtime/PlayerObjectTests.cs
@@ -11,7 +11,7 @@ namespace Unity.Netcode.RuntimeTests
[TestFixture(HostOrServer.Server)]
internal class PlayerObjectTests : NetcodeIntegrationTest
{
- protected override int NumberOfClients => 1;
+ protected override int NumberOfClients => 2;
protected GameObject m_NewPlayerToSpawn;
@@ -52,4 +52,136 @@ namespace Unity.Netcode.RuntimeTests
Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side player object to change!");
}
}
+
+ ///
+ /// Validate that when auto-player spawning but SpawnWithObservers is disabled,
+ /// the player instantiated is only spawned on the authority side.
+ ///
+ [TestFixture(HostOrServer.DAHost)]
+ [TestFixture(HostOrServer.Host)]
+ [TestFixture(HostOrServer.Server)]
+ internal class PlayerSpawnNoObserversTest : NetcodeIntegrationTest
+ {
+ protected override int NumberOfClients => 2;
+
+ public PlayerSpawnNoObserversTest(HostOrServer hostOrServer) : base(hostOrServer) { }
+
+ protected override bool ShouldCheckForSpawnedPlayers()
+ {
+ return false;
+ }
+
+ protected override void OnCreatePlayerPrefab()
+ {
+ var playerNetworkObject = m_PlayerPrefab.GetComponent();
+ playerNetworkObject.SpawnWithObservers = false;
+ base.OnCreatePlayerPrefab();
+ }
+
+ [UnityTest]
+ public IEnumerator SpawnWithNoObservers()
+ {
+ yield return s_DefaultWaitForTick;
+
+ if (!m_DistributedAuthority)
+ {
+ // Make sure clients did not spawn their player object on any of the clients including the owner.
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ foreach (var playerObject in m_ServerNetworkManager.SpawnManager.PlayerObjects)
+ {
+ Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObject.NetworkObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{playerObject.NetworkObjectId}!");
+ }
+ }
+ }
+ else
+ {
+ // For distributed authority, we want to make sure the player object is only spawned on the authority side and all non-authority instances did not spawn it.
+ var playerObjectId = m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId;
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{m_ServerNetworkManager.LocalClientId}!");
+ }
+
+ foreach (var clientPlayer in m_ClientNetworkManagers)
+ {
+ playerObjectId = clientPlayer.LocalClient.PlayerObject.NetworkObjectId;
+ Assert.IsFalse(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{m_ServerNetworkManager.LocalClientId} spawned player object for Client-{clientPlayer.LocalClientId}!");
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ if (clientPlayer == client)
+ {
+ continue;
+ }
+ Assert.IsFalse(client.SpawnManager.SpawnedObjects.ContainsKey(playerObjectId), $"Client-{client.LocalClientId} spawned player object for Client-{clientPlayer.LocalClientId}!");
+ }
+ }
+
+ }
+ }
+ }
+
+ ///
+ /// This test validates the player position and rotation is correct
+ /// relative to the prefab's initial settings if no changes are applied.
+ ///
+ [TestFixture(HostOrServer.DAHost)]
+ [TestFixture(HostOrServer.Host)]
+ [TestFixture(HostOrServer.Server)]
+ internal class PlayerSpawnPositionTests : IntegrationTestWithApproximation
+ {
+ protected override int NumberOfClients => 2;
+
+ public PlayerSpawnPositionTests(HostOrServer hostOrServer) : base(hostOrServer) { }
+
+ private Vector3 m_PlayerPosition;
+ private Quaternion m_PlayerRotation;
+
+ protected override void OnCreatePlayerPrefab()
+ {
+ var playerNetworkObject = m_PlayerPrefab.GetComponent();
+ m_PlayerPosition = GetRandomVector3(-10.0f, 10.0f);
+ m_PlayerRotation = Quaternion.Euler(GetRandomVector3(-180.0f, 180.0f));
+ playerNetworkObject.transform.position = m_PlayerPosition;
+ playerNetworkObject.transform.rotation = m_PlayerRotation;
+ base.OnCreatePlayerPrefab();
+ }
+
+ private void PlayerTransformMatches(NetworkObject player)
+ {
+ var position = player.transform.position;
+ var rotation = player.transform.rotation;
+ Assert.True(Approximately(m_PlayerPosition, position), $"Client-{player.OwnerClientId} position {position} does not match the prefab position {m_PlayerPosition}!");
+ Assert.True(Approximately(m_PlayerRotation, rotation), $"Client-{player.OwnerClientId} rotation {rotation.eulerAngles} does not match the prefab rotation {m_PlayerRotation.eulerAngles}!");
+ }
+
+ [UnityTest]
+ public IEnumerator PlayerSpawnPosition()
+ {
+ if (m_ServerNetworkManager.IsHost)
+ {
+ PlayerTransformMatches(m_ServerNetworkManager.LocalClient.PlayerObject);
+
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ yield return WaitForConditionOrTimeOut(() => client.SpawnManager.SpawnedObjects.ContainsKey(m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId));
+ AssertOnTimeout($"Client-{client.LocalClientId} does not contain a player prefab instance for client-{m_ServerNetworkManager.LocalClientId}!");
+ PlayerTransformMatches(client.SpawnManager.SpawnedObjects[m_ServerNetworkManager.LocalClient.PlayerObject.NetworkObjectId]);
+ }
+ }
+
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ yield return WaitForConditionOrTimeOut(() => m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(client.LocalClient.PlayerObject.NetworkObjectId));
+ AssertOnTimeout($"Client-{m_ServerNetworkManager.LocalClientId} does not contain a player prefab instance for client-{client.LocalClientId}!");
+ PlayerTransformMatches(m_ServerNetworkManager.SpawnManager.SpawnedObjects[client.LocalClient.PlayerObject.NetworkObjectId]);
+ foreach (var subClient in m_ClientNetworkManagers)
+ {
+ yield return WaitForConditionOrTimeOut(() => subClient.SpawnManager.SpawnedObjects.ContainsKey(client.LocalClient.PlayerObject.NetworkObjectId));
+ AssertOnTimeout($"Client-{subClient.LocalClientId} does not contain a player prefab instance for client-{client.LocalClientId}!");
+ PlayerTransformMatches(subClient.SpawnManager.SpawnedObjects[client.LocalClient.PlayerObject.NetworkObjectId]);
+ }
+ }
+ }
+ }
}
diff --git a/package.json b/package.json
index 049964c..ab43e49 100644
--- a/package.json
+++ b/package.json
@@ -2,23 +2,23 @@
"name": "com.unity.netcode.gameobjects",
"displayName": "Netcode for GameObjects",
"description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.",
- "version": "2.0.0",
+ "version": "2.1.1",
"unity": "6000.0",
"dependencies": {
"com.unity.nuget.mono-cecil": "1.11.4",
"com.unity.transport": "2.3.0"
},
"_upm": {
- "changelog": "### Added\n\n- Added tooltips for all of the `NetworkObject` component's properties. (#3052)\n- Added message size validation to named and unnamed message sending functions for better error messages. (#3049)\n- Added \"Check for NetworkObject Component\" property to the Multiplayer->Netcode for GameObjects project settings. When disabled, this will bypass the in-editor `NetworkObject` check on `NetworkBehaviour` components. (#3031)\n- Added `NetworkTransform.SwitchTransformSpaceWhenParented` property that, when enabled, will handle the world to local, local to world, and local to local transform space transitions when interpolation is enabled. (#3013)\n- Added `NetworkTransform.TickSyncChildren` that, when enabled, will tick synchronize nested and/or child `NetworkTransform` components to eliminate any potential visual jittering that could occur if the `NetworkTransform` instances get into a state where their state updates are landing on different network ticks. (#3013)\n- Added `NetworkObject.AllowOwnerToParent` property to provide the ability to allow clients to parent owned objects when running in a client-server network topology. (#3013)\n- Added `NetworkObject.SyncOwnerTransformWhenParented` property to provide a way to disable applying the server's transform information in the parenting message on the client owner instance which can be useful for owner authoritative motion models. (#3013)\n- Added `NetcodeEditorBase` editor helper class to provide easier modification and extension of the SDK's components. (#3013)\n\n### Fixed\n\n- Fixed issue where `NetworkAnimator` would send updates to non-observer clients. (#3057)\n- Fixed issue where an exception could occur when receiving a universal RPC for a `NetworkObject` that has been despawned. (#3052)\n- Fixed issue where a NetworkObject hidden from a client that is then promoted to be session owner was not being synchronized with newly joining clients.(#3051)\n- Fixed issue where clients could have a wrong time delta on `NetworkVariableBase` which could prevent from sending delta state updates. (#3045)\n- Fixed issue where setting a prefab hash value during connection approval but not having a player prefab assigned could cause an exception when spawning a player. (#3042)\n- Fixed issue where the `NetworkSpawnManager.HandleNetworkObjectShow` could throw an exception if one of the `NetworkObject` components to show was destroyed during the same frame. (#3030)\n- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3026)\n- Fixed issue with newly/late joined clients and `NetworkTransform` synchronization of parented `NetworkObject` instances. (#3013)\n- Fixed issue with smooth transitions between transform spaces when interpolation is enabled (requires `NetworkTransform.SwitchTransformSpaceWhenParented` to be enabled). (#3013)\n\n### Changed\n\n- Changed `NetworkTransformEditor` now uses `NetworkTransform` as the base type class to assure it doesn't display a foldout group when using the base `NetworkTransform` component class. (#3052)\n- Changed `NetworkAnimator.Awake` is now a protected virtual method. (#3052)\n- Changed when invoking `NetworkManager.ConnectionManager.DisconnectClient` during a distributed authority session a more appropriate message is logged. (#3052)\n- Changed `NetworkTransformEditor` so it now derives from `NetcodeEditorBase`. (#3013)\n- Changed `NetworkRigidbodyBaseEditor` so it now derives from `NetcodeEditorBase`. (#3013)\n- Changed `NetworkManagerEditor` so it now derives from `NetcodeEditorBase`. (#3013)"
+ "changelog": "### Added\n\n- Added ability to edit the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` within the inspector view. (#3097)\n- Added `IContactEventHandlerWithInfo` that derives from `IContactEventHandler` that can be updated per frame to provide `ContactEventHandlerInfo` information to the `RigidbodyContactEventManager` when processing collisions. (#3094)\n - `ContactEventHandlerInfo.ProvideNonRigidBodyContactEvents`: When set to true, non-`Rigidbody` collisions with the registered `Rigidbody` will generate contact event notifications. (#3094)\n - `ContactEventHandlerInfo.HasContactEventPriority`: When set to true, the `Rigidbody` will be prioritized as the instance that generates the event if the `Rigidbody` colliding does not have priority. (#3094)\n- Added a static `NetworkManager.OnInstantiated` event notification to be able to track when a new `NetworkManager` instance has been instantiated. (#3088)\n- Added a static `NetworkManager.OnDestroying` event notification to be able to track when an existing `NetworkManager` instance is being destroyed. (#3088)\n\n### Fixed\n\n- Fixed issue where `NetworkPrefabProcessor` would not mark the prefab list as dirty and prevent saving the `DefaultNetworkPrefabs` asset when only imports or only deletes were detected.(#3103)\n- Fixed an issue where nested `NetworkTransform` components in owner authoritative mode cleared their initial settings on the server, causing improper synchronization. (#3099)\n- Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096)\n- Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092)\n- Fixed an issue where newly synchronizing clients would always receive current `NetworkVariable` values, potentially causing issues with collections if there were pending updates. Now, pending state updates serialize previous values to avoid duplicates on new clients. (#3081)\n- Fixed issue where changing ownership would mark every `NetworkVariable` dirty. Now, it will only mark any `NetworkVariable` with owner read permissions as dirty and will send/flush any pending updates to all clients prior to sending the change in ownership message. (#3081)\n- Fixed an issue where transferring ownership of `NetworkVariable` collections didn't update the new owner’s previous value, causing the last added value to be detected as a change during additions or removals. (#3081)\n- Fixed issue where a client (or server) with no write permissions for a `NetworkVariable` using a standard .NET collection type could still modify the collection which could cause various issues depending upon the modification and collection type. (#3081)\n- Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078)\n- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077)\n- Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075)\n\n### Changed\n\n- Changed `NetworkConfig.AutoSpawnPlayerPrefabClientSide` is no longer automatically set when starting `NetworkManager`. (#3097)\n- Updated `NetworkVariableDeltaMessage` so the server now forwards delta state updates from clients immediately, instead of waiting until the end of the frame or the next network tick. (#3081)"
},
"upmCi": {
- "footprint": "f1ef7566b7a89b1ee9c34cc13400735ae63964d4"
+ "footprint": "8331c76150e539e36659d8b7be3ba0fb6d21027a"
},
- "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.0/manual/index.html",
+ "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.1/manual/index.html",
"repository": {
"url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git",
"type": "git",
- "revision": "8a7ae9f91a53bdcabe5e7df783dd1884c07bcd6f"
+ "revision": "264b30d176dd71fcedd022a8d6f4d59a2e3922bc"
},
"samples": [
{