diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index f3b9c50ffd..2aeef42809 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -20,6 +20,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
+- 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)
- 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)
- 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)
- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3026)
diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs
index 03d64cb1fc..af97de8d06 100644
--- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs
@@ -985,10 +985,18 @@ internal NetworkClient AddClient(ulong clientId)
ConnectedClientIds.Add(clientId);
}
+ var distributedAuthority = NetworkManager.DistributedAuthorityMode;
+ var sessionOwnerId = NetworkManager.CurrentSessionOwner;
+ var isSessionOwner = NetworkManager.LocalClient.IsSessionOwner;
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
{
if (networkObject.SpawnWithObservers)
{
+ // Don't add the client to the observers if hidden from the session owner
+ if (networkObject.IsOwner && distributedAuthority && !isSessionOwner && !networkObject.Observers.Contains(sessionOwnerId))
+ {
+ continue;
+ }
networkObject.Observers.Add(clientId);
}
}
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index 1239c93c7e..0cdb995128 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -195,7 +195,6 @@ internal void SetSessionOwner(ulong sessionOwner)
OnSessionOwnerPromoted?.Invoke(sessionOwner);
}
- // TODO: Make this internal after testing
internal void PromoteSessionOwner(ulong clientId)
{
if (!DistributedAuthorityMode)
diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs
index 0a1b1aeeb4..d1cd7e43eb 100644
--- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs
@@ -55,6 +55,12 @@ public void Handle(ref NetworkContext context)
// Don't redistribute for the local instance
if (ClientId != networkManager.LocalClientId)
{
+ // Show any NetworkObjects that are:
+ // - Hidden from the session owner
+ // - Owned by this client
+ // - Has NetworkObject.SpawnWithObservers set to true (the default)
+ networkManager.SpawnManager.ShowHiddenObjectsToNewlyJoinedClient(ClientId);
+
// We defer redistribution to the end of the NetworkUpdateStage.PostLateUpdate
networkManager.RedistributeToClient = true;
networkManager.ClientToRedistribute = ClientId;
diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
index 975697862b..65222007bf 100644
--- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs
@@ -2550,17 +2550,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
// At this point the client is considered fully "connected"
if ((NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClient.IsSessionOwner) || !NetworkManager.DistributedAuthorityMode)
{
- if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
- {
- // DANGO-EXP TODO: Remove this once service is sending the synchronization message to all clients
- if (NetworkManager.ConnectedClients.ContainsKey(clientId) && NetworkManager.ConnectionManager.ConnectedClientIds.Contains(clientId) && NetworkManager.ConnectedClientsList.Contains(NetworkManager.ConnectedClients[clientId]))
- {
- EndSceneEvent(sceneEventId);
- return;
- }
- NetworkManager.ConnectionManager.AddClient(clientId);
- }
-
// Notify the local server that a client has finished synchronizing
OnSceneEvent?.Invoke(new SceneEvent()
{
@@ -2575,6 +2564,20 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
}
else
{
+ // Notify the local server that a client has finished synchronizing
+ OnSceneEvent?.Invoke(new SceneEvent()
+ {
+ SceneEventType = sceneEventData.SceneEventType,
+ SceneName = string.Empty,
+ ClientId = clientId
+ });
+
+ // Show any NetworkObjects that are:
+ // - Hidden from the session owner
+ // - Owned by this client
+ // - Has NetworkObject.SpawnWithObservers set to true (the default)
+ NetworkManager.SpawnManager.ShowHiddenObjectsToNewlyJoinedClient(clientId);
+
// DANGO-EXP TODO: Remove this once service distributes objects
// Non-session owners receive this notification from newly connected clients and upon receiving
// the event they will redistribute their NetworkObjects
@@ -2589,9 +2592,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
// At this time the client is fully synchronized with all loaded scenes and
// NetworkObjects and should be considered "fully connected". Send the
// notification that the client is connected.
- // TODO 2023: We should have a better name for this or have multiple states the
- // client progresses through (the name and associated legacy behavior/expected state
- // of the client was persisted since MLAPI)
NetworkManager.ConnectionManager.InvokeOnClientConnectedCallback(clientId);
if (NetworkManager.IsHost)
@@ -2664,9 +2664,14 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
EventData = sceneEventData,
};
// Forward synchronization to client then exit early because DAHost is not the current session owner
- NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.CurrentSessionOwner);
- EndSceneEvent(sceneEventData.SceneEventId);
- return;
+ foreach (var client in NetworkManager.ConnectedClientsIds)
+ {
+ if (client == NetworkManager.LocalClientId)
+ {
+ continue;
+ }
+ NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, client);
+ }
}
}
else
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
index 4a7ea434ec..d3b2f9acd6 100644
--- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
@@ -1909,5 +1909,55 @@ internal void NotifyNetworkObjectsSynchronized()
networkObject.InternalNetworkSessionSynchronized();
}
}
+
+ ///
+ /// Distributed Authority Only
+ /// Should be invoked on non-session owner clients when a newly joined client is finished
+ /// synchronizing in order to "show" (spawn) anything that might be currently hidden from
+ /// the session owner.
+ ///
+ internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId)
+ {
+ if (!NetworkManager.DistributedAuthorityMode)
+ {
+ if (NetworkManager == null || !NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer)
+ {
+ Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while !");
+ }
+ return;
+ }
+
+ if (!NetworkManager.DistributedAuthorityMode)
+ {
+ Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!");
+ return;
+ }
+
+ if (NetworkManager.LocalClient.IsSessionOwner)
+ {
+ Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!");
+ return;
+ }
+ var localClientId = NetworkManager.LocalClient.ClientId;
+ var sessionOwnerId = NetworkManager.CurrentSessionOwner;
+ foreach (var networkObject in SpawnedObjectsList)
+ {
+ if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId))
+ {
+ if (networkObject.Observers.Contains(newClientId))
+ {
+ if (NetworkManager.LogLevel <= LogLevel.Developer)
+ {
+ // Track if there is some other location where the client is being added to the observers list when the object is hidden from the session owner
+ Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!");
+ }
+ // For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added
+ // code to handle this edge case works.
+ networkObject.Observers.Remove(newClientId);
+ }
+ networkObject.NetworkShow(newClientId);
+ }
+ }
+ }
}
}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs
new file mode 100644
index 0000000000..61d71c0d4d
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs
@@ -0,0 +1,139 @@
+using System.Collections;
+using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace Unity.Netcode.RuntimeTests
+{
+ [TestFixture(HostOrServer.DAHost)]
+ public class ExtendedNetworkShowAndHideTests : NetcodeIntegrationTest
+ {
+ protected override int NumberOfClients => 3;
+
+ private GameObject m_ObjectToSpawn;
+ private NetworkObject m_SpawnedObject;
+ private NetworkManager m_ClientToHideFrom;
+ private NetworkManager m_LateJoinClient;
+ private NetworkManager m_SpawnOwner;
+
+ public ExtendedNetworkShowAndHideTests(HostOrServer hostOrServer) : base(hostOrServer) { }
+
+ protected override void OnServerAndClientsCreated()
+ {
+ m_ObjectToSpawn = CreateNetworkObjectPrefab("TestObject");
+ m_ObjectToSpawn.SetActive(false);
+ base.OnServerAndClientsCreated();
+ }
+
+ private bool AllClientsSpawnedObject()
+ {
+ if (!UseCMBService())
+ {
+ if (!s_GlobalNetworkObjects.ContainsKey(m_ServerNetworkManager.LocalClientId))
+ {
+ return false;
+ }
+ if (!s_GlobalNetworkObjects[m_ServerNetworkManager.LocalClientId].ContainsKey(m_SpawnedObject.NetworkObjectId))
+ {
+ return false;
+ }
+ }
+
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ if (!s_GlobalNetworkObjects.ContainsKey(client.LocalClientId))
+ {
+ return false;
+ }
+ if (!s_GlobalNetworkObjects[client.LocalClientId].ContainsKey(m_SpawnedObject.NetworkObjectId))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool IsClientPromotedToSessionOwner()
+ {
+ if (!UseCMBService())
+ {
+ if (m_ServerNetworkManager.CurrentSessionOwner != m_ClientToHideFrom.LocalClientId)
+ {
+ return false;
+ }
+ }
+
+ foreach (var client in m_ClientNetworkManagers)
+ {
+ if (!client.IsConnectedClient)
+ {
+ continue;
+ }
+ if (client.CurrentSessionOwner != m_ClientToHideFrom.LocalClientId)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ protected override void OnNewClientCreated(NetworkManager networkManager)
+ {
+ m_LateJoinClient = networkManager;
+
+ networkManager.NetworkConfig.Prefabs = m_SpawnOwner.NetworkConfig.Prefabs;
+ base.OnNewClientCreated(networkManager);
+ }
+
+ ///
+ /// This test validates the following NetworkShow - NetworkHide issue:
+ /// - During a session, a spawned object is hidden from a client.
+ /// - The current session owner disconnects and the client the object is hidden from is prommoted to the session owner.
+ /// - A new client joins and the newly promoted session owner synchronizes the newly joined client with only objects visible to it.
+ /// - Any already connected non-session owner client should "NetworkShow" the object to the newly connected client
+ /// (but only if the hidden object has SpawnWithObservers enabled)
+ ///
+ [UnityTest]
+ public IEnumerator HiddenObjectPromotedSessionOwnerNewClientSynchronizes()
+ {
+ // Get the test relative session owner
+ var sessionOwner = UseCMBService() ? m_ClientNetworkManagers[0] : m_ServerNetworkManager;
+ m_SpawnOwner = UseCMBService() ? m_ClientNetworkManagers[1] : m_ClientNetworkManagers[0];
+ m_ClientToHideFrom = UseCMBService() ? m_ClientNetworkManagers[NumberOfClients - 1] : m_ClientNetworkManagers[1];
+ m_ObjectToSpawn.SetActive(true);
+
+ // Spawn the object with a non-session owner client
+ m_SpawnedObject = SpawnObject(m_ObjectToSpawn, m_SpawnOwner).GetComponent();
+ yield return WaitForConditionOrTimeOut(AllClientsSpawnedObject);
+ AssertOnTimeout($"Not all clients spawned and instance of {m_SpawnedObject.name}");
+
+ // Hide the spawned object from the to be promoted session owner
+ m_SpawnedObject.NetworkHide(m_ClientToHideFrom.LocalClientId);
+
+ yield return WaitForConditionOrTimeOut(() => !m_ClientToHideFrom.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedObject.NetworkObjectId));
+ AssertOnTimeout($"{m_SpawnedObject.name} was not hidden from Client-{m_ClientToHideFrom.LocalClientId}!");
+
+ // Promoted a new session owner (DAHost promotes while CMB Session we disconnect the current session owner)
+ if (!UseCMBService())
+ {
+ m_ServerNetworkManager.PromoteSessionOwner(m_ClientToHideFrom.LocalClientId);
+ }
+ else
+ {
+ sessionOwner.Shutdown();
+ }
+
+ // Wait for the new session owner to be promoted and for all clients to acknowledge the promotion
+ yield return WaitForConditionOrTimeOut(IsClientPromotedToSessionOwner);
+ AssertOnTimeout($"Client-{m_ClientToHideFrom.LocalClientId} was not promoted as session owner on all client instances!");
+
+ // Connect a new client instance
+ yield return CreateAndStartNewClient();
+
+ // Assure the newly connected client is synchronized with the NetworkObject hidden from the newly promoted session owner
+ yield return WaitForConditionOrTimeOut(() => m_LateJoinClient.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedObject.NetworkObjectId));
+ AssertOnTimeout($"Client-{m_LateJoinClient.LocalClientId} never spawned {nameof(NetworkObject)} {m_SpawnedObject.name}!");
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta
new file mode 100644
index 0000000000..5d227513b6
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a6389d04d9080b24b99de7e6900a064c
\ No newline at end of file