Skip to content

Commit 91bb80e

Browse files
fix: hidden objects from newly promoted session owner still synchronize with newly joining clients (#3051)
* fix This fixes the issue where a NetworkObject hidden from a client that is promoted to session owner will still be synchronized with newly joining clients. * test The test to validate the fix * update adding changelog entry * style Minor typo
1 parent 8e62d9e commit 91bb80e

File tree

8 files changed

+228
-18
lines changed

8 files changed

+228
-18
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2020

2121
### Fixed
2222

23+
- 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)
2324
- 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)
2425
- 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)
2526
- Fixed issue where the `NetworkManagerHelper` was continuing to check for hierarchy changes when in play mode. (#3026)

com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,10 +985,18 @@ internal NetworkClient AddClient(ulong clientId)
985985
ConnectedClientIds.Add(clientId);
986986
}
987987

988+
var distributedAuthority = NetworkManager.DistributedAuthorityMode;
989+
var sessionOwnerId = NetworkManager.CurrentSessionOwner;
990+
var isSessionOwner = NetworkManager.LocalClient.IsSessionOwner;
988991
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
989992
{
990993
if (networkObject.SpawnWithObservers)
991994
{
995+
// Don't add the client to the observers if hidden from the session owner
996+
if (networkObject.IsOwner && distributedAuthority && !isSessionOwner && !networkObject.Observers.Contains(sessionOwnerId))
997+
{
998+
continue;
999+
}
9921000
networkObject.Observers.Add(clientId);
9931001
}
9941002
}

com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ internal void SetSessionOwner(ulong sessionOwner)
195195
OnSessionOwnerPromoted?.Invoke(sessionOwner);
196196
}
197197

198-
// TODO: Make this internal after testing
199198
internal void PromoteSessionOwner(ulong clientId)
200199
{
201200
if (!DistributedAuthorityMode)

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public void Handle(ref NetworkContext context)
5555
// Don't redistribute for the local instance
5656
if (ClientId != networkManager.LocalClientId)
5757
{
58+
// Show any NetworkObjects that are:
59+
// - Hidden from the session owner
60+
// - Owned by this client
61+
// - Has NetworkObject.SpawnWithObservers set to true (the default)
62+
networkManager.SpawnManager.ShowHiddenObjectsToNewlyJoinedClient(ClientId);
63+
5864
// We defer redistribution to the end of the NetworkUpdateStage.PostLateUpdate
5965
networkManager.RedistributeToClient = true;
6066
networkManager.ClientToRedistribute = ClientId;

com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2550,17 +2550,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
25502550
// At this point the client is considered fully "connected"
25512551
if ((NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClient.IsSessionOwner) || !NetworkManager.DistributedAuthorityMode)
25522552
{
2553-
if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost)
2554-
{
2555-
// DANGO-EXP TODO: Remove this once service is sending the synchronization message to all clients
2556-
if (NetworkManager.ConnectedClients.ContainsKey(clientId) && NetworkManager.ConnectionManager.ConnectedClientIds.Contains(clientId) && NetworkManager.ConnectedClientsList.Contains(NetworkManager.ConnectedClients[clientId]))
2557-
{
2558-
EndSceneEvent(sceneEventId);
2559-
return;
2560-
}
2561-
NetworkManager.ConnectionManager.AddClient(clientId);
2562-
}
2563-
25642553
// Notify the local server that a client has finished synchronizing
25652554
OnSceneEvent?.Invoke(new SceneEvent()
25662555
{
@@ -2575,6 +2564,20 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
25752564
}
25762565
else
25772566
{
2567+
// Notify the local server that a client has finished synchronizing
2568+
OnSceneEvent?.Invoke(new SceneEvent()
2569+
{
2570+
SceneEventType = sceneEventData.SceneEventType,
2571+
SceneName = string.Empty,
2572+
ClientId = clientId
2573+
});
2574+
2575+
// Show any NetworkObjects that are:
2576+
// - Hidden from the session owner
2577+
// - Owned by this client
2578+
// - Has NetworkObject.SpawnWithObservers set to true (the default)
2579+
NetworkManager.SpawnManager.ShowHiddenObjectsToNewlyJoinedClient(clientId);
2580+
25782581
// DANGO-EXP TODO: Remove this once service distributes objects
25792582
// Non-session owners receive this notification from newly connected clients and upon receiving
25802583
// the event they will redistribute their NetworkObjects
@@ -2589,9 +2592,6 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
25892592
// At this time the client is fully synchronized with all loaded scenes and
25902593
// NetworkObjects and should be considered "fully connected". Send the
25912594
// notification that the client is connected.
2592-
// TODO 2023: We should have a better name for this or have multiple states the
2593-
// client progresses through (the name and associated legacy behavior/expected state
2594-
// of the client was persisted since MLAPI)
25952595
NetworkManager.ConnectionManager.InvokeOnClientConnectedCallback(clientId);
25962596

25972597
if (NetworkManager.IsHost)
@@ -2664,9 +2664,14 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader)
26642664
EventData = sceneEventData,
26652665
};
26662666
// Forward synchronization to client then exit early because DAHost is not the current session owner
2667-
NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.CurrentSessionOwner);
2668-
EndSceneEvent(sceneEventData.SceneEventId);
2669-
return;
2667+
foreach (var client in NetworkManager.ConnectedClientsIds)
2668+
{
2669+
if (client == NetworkManager.LocalClientId)
2670+
{
2671+
continue;
2672+
}
2673+
NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, client);
2674+
}
26702675
}
26712676
}
26722677
else

com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,5 +1909,55 @@ internal void NotifyNetworkObjectsSynchronized()
19091909
networkObject.InternalNetworkSessionSynchronized();
19101910
}
19111911
}
1912+
1913+
/// <summary>
1914+
/// Distributed Authority Only
1915+
/// Should be invoked on non-session owner clients when a newly joined client is finished
1916+
/// synchronizing in order to "show" (spawn) anything that might be currently hidden from
1917+
/// the session owner.
1918+
/// </summary>
1919+
internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId)
1920+
{
1921+
if (!NetworkManager.DistributedAuthorityMode)
1922+
{
1923+
if (NetworkManager == null || !NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer)
1924+
{
1925+
Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while !");
1926+
}
1927+
return;
1928+
}
1929+
1930+
if (!NetworkManager.DistributedAuthorityMode)
1931+
{
1932+
Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!");
1933+
return;
1934+
}
1935+
1936+
if (NetworkManager.LocalClient.IsSessionOwner)
1937+
{
1938+
Debug.LogError($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} should only be invoked on a non-session owner client!");
1939+
return;
1940+
}
1941+
var localClientId = NetworkManager.LocalClient.ClientId;
1942+
var sessionOwnerId = NetworkManager.CurrentSessionOwner;
1943+
foreach (var networkObject in SpawnedObjectsList)
1944+
{
1945+
if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId && !networkObject.Observers.Contains(sessionOwnerId))
1946+
{
1947+
if (networkObject.Observers.Contains(newClientId))
1948+
{
1949+
if (NetworkManager.LogLevel <= LogLevel.Developer)
1950+
{
1951+
// 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
1952+
Debug.LogWarning($"[{networkObject.name}] Has new client as an observer but it is hidden from the session owner!");
1953+
}
1954+
// 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
1955+
// code to handle this edge case works.
1956+
networkObject.Observers.Remove(newClientId);
1957+
}
1958+
networkObject.NetworkShow(newClientId);
1959+
}
1960+
}
1961+
}
19121962
}
19131963
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System.Collections;
2+
using NUnit.Framework;
3+
using Unity.Netcode.TestHelpers.Runtime;
4+
using UnityEngine;
5+
using UnityEngine.TestTools;
6+
7+
namespace Unity.Netcode.RuntimeTests
8+
{
9+
[TestFixture(HostOrServer.DAHost)]
10+
public class ExtendedNetworkShowAndHideTests : NetcodeIntegrationTest
11+
{
12+
protected override int NumberOfClients => 3;
13+
14+
private GameObject m_ObjectToSpawn;
15+
private NetworkObject m_SpawnedObject;
16+
private NetworkManager m_ClientToHideFrom;
17+
private NetworkManager m_LateJoinClient;
18+
private NetworkManager m_SpawnOwner;
19+
20+
public ExtendedNetworkShowAndHideTests(HostOrServer hostOrServer) : base(hostOrServer) { }
21+
22+
protected override void OnServerAndClientsCreated()
23+
{
24+
m_ObjectToSpawn = CreateNetworkObjectPrefab("TestObject");
25+
m_ObjectToSpawn.SetActive(false);
26+
base.OnServerAndClientsCreated();
27+
}
28+
29+
private bool AllClientsSpawnedObject()
30+
{
31+
if (!UseCMBService())
32+
{
33+
if (!s_GlobalNetworkObjects.ContainsKey(m_ServerNetworkManager.LocalClientId))
34+
{
35+
return false;
36+
}
37+
if (!s_GlobalNetworkObjects[m_ServerNetworkManager.LocalClientId].ContainsKey(m_SpawnedObject.NetworkObjectId))
38+
{
39+
return false;
40+
}
41+
}
42+
43+
foreach (var client in m_ClientNetworkManagers)
44+
{
45+
if (!s_GlobalNetworkObjects.ContainsKey(client.LocalClientId))
46+
{
47+
return false;
48+
}
49+
if (!s_GlobalNetworkObjects[client.LocalClientId].ContainsKey(m_SpawnedObject.NetworkObjectId))
50+
{
51+
return false;
52+
}
53+
}
54+
return true;
55+
}
56+
57+
private bool IsClientPromotedToSessionOwner()
58+
{
59+
if (!UseCMBService())
60+
{
61+
if (m_ServerNetworkManager.CurrentSessionOwner != m_ClientToHideFrom.LocalClientId)
62+
{
63+
return false;
64+
}
65+
}
66+
67+
foreach (var client in m_ClientNetworkManagers)
68+
{
69+
if (!client.IsConnectedClient)
70+
{
71+
continue;
72+
}
73+
if (client.CurrentSessionOwner != m_ClientToHideFrom.LocalClientId)
74+
{
75+
return false;
76+
}
77+
}
78+
return true;
79+
}
80+
81+
protected override void OnNewClientCreated(NetworkManager networkManager)
82+
{
83+
m_LateJoinClient = networkManager;
84+
85+
networkManager.NetworkConfig.Prefabs = m_SpawnOwner.NetworkConfig.Prefabs;
86+
base.OnNewClientCreated(networkManager);
87+
}
88+
89+
/// <summary>
90+
/// This test validates the following NetworkShow - NetworkHide issue:
91+
/// - During a session, a spawned object is hidden from a client.
92+
/// - The current session owner disconnects and the client the object is hidden from is prommoted to the session owner.
93+
/// - A new client joins and the newly promoted session owner synchronizes the newly joined client with only objects visible to it.
94+
/// - Any already connected non-session owner client should "NetworkShow" the object to the newly connected client
95+
/// (but only if the hidden object has SpawnWithObservers enabled)
96+
/// </summary>
97+
[UnityTest]
98+
public IEnumerator HiddenObjectPromotedSessionOwnerNewClientSynchronizes()
99+
{
100+
// Get the test relative session owner
101+
var sessionOwner = UseCMBService() ? m_ClientNetworkManagers[0] : m_ServerNetworkManager;
102+
m_SpawnOwner = UseCMBService() ? m_ClientNetworkManagers[1] : m_ClientNetworkManagers[0];
103+
m_ClientToHideFrom = UseCMBService() ? m_ClientNetworkManagers[NumberOfClients - 1] : m_ClientNetworkManagers[1];
104+
m_ObjectToSpawn.SetActive(true);
105+
106+
// Spawn the object with a non-session owner client
107+
m_SpawnedObject = SpawnObject(m_ObjectToSpawn, m_SpawnOwner).GetComponent<NetworkObject>();
108+
yield return WaitForConditionOrTimeOut(AllClientsSpawnedObject);
109+
AssertOnTimeout($"Not all clients spawned and instance of {m_SpawnedObject.name}");
110+
111+
// Hide the spawned object from the to be promoted session owner
112+
m_SpawnedObject.NetworkHide(m_ClientToHideFrom.LocalClientId);
113+
114+
yield return WaitForConditionOrTimeOut(() => !m_ClientToHideFrom.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedObject.NetworkObjectId));
115+
AssertOnTimeout($"{m_SpawnedObject.name} was not hidden from Client-{m_ClientToHideFrom.LocalClientId}!");
116+
117+
// Promoted a new session owner (DAHost promotes while CMB Session we disconnect the current session owner)
118+
if (!UseCMBService())
119+
{
120+
m_ServerNetworkManager.PromoteSessionOwner(m_ClientToHideFrom.LocalClientId);
121+
}
122+
else
123+
{
124+
sessionOwner.Shutdown();
125+
}
126+
127+
// Wait for the new session owner to be promoted and for all clients to acknowledge the promotion
128+
yield return WaitForConditionOrTimeOut(IsClientPromotedToSessionOwner);
129+
AssertOnTimeout($"Client-{m_ClientToHideFrom.LocalClientId} was not promoted as session owner on all client instances!");
130+
131+
// Connect a new client instance
132+
yield return CreateAndStartNewClient();
133+
134+
// Assure the newly connected client is synchronized with the NetworkObject hidden from the newly promoted session owner
135+
yield return WaitForConditionOrTimeOut(() => m_LateJoinClient.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedObject.NetworkObjectId));
136+
AssertOnTimeout($"Client-{m_LateJoinClient.LocalClientId} never spawned {nameof(NetworkObject)} {m_SpawnedObject.name}!");
137+
}
138+
}
139+
}

com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/ExtendedNetworkShowAndHideTests.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)