Skip to content

Commit d80340d

Browse files
fix: distributed authority client synchronization issues (#3350)
This PR resolves two client synchronization related issues: - When using a distributed authority network topology the session owner can max-out the reliable in-flight messages allowed and start dropping packets when many clients attempt to connect simultaneously. - When scene management was disabled, any client attempting to spawn objects during the initial synchronization would not be allowed to due to the `NetworkManager.IsConnectedClient` not being set until after the client had finished synchronizing (i.e. all objects synchronized had run through the spawn process). fix: #3280 close: #3280 ## Changelog - Fixed: Issue when using a distributed authority network topology and many clients attempt to connect simultaneously the session owner could max-out the maximum in-flight reliable messages allowed, start dropping packets, and some of the connecting clients would fail to fully synchronize. - Fixed: Issue when using a distributed authority network topology and scene management was disabled clients would not be able to spawn any new network prefab instances until synchronization was complete. ## Testing and Documentation - No test has been added for session owner reaching maximum in-flight reliable messages (requires manual CCU stress test). - Includes the `SpawnDuringSynchronizationTests` integration test. - No documentation changes or additions were necessary. <!-- Uncomment and mark items off with a * if this PR deprecates any API: ### Deprecated API - [ ] An `[Obsolete]` attribute was added along with a `(RemovedAfter yyyy-mm-dd)` entry. - [ ] An [api updater] was added. - [ ] Deprecation of the API is explained in the CHANGELOG. - [ ] The users can understand why this API was removed and what they should use instead. --> --------- Co-authored-by: Emma <[email protected]>
1 parent cab4d78 commit d80340d

File tree

7 files changed

+241
-18
lines changed

7 files changed

+241
-18
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
1818

1919
### Fixed
2020

21+
- Fixed issue when using a distributed authority network topology and many clients attempt to connect simultaneously the session owner could max-out the maximum in-flight reliable messages allowed, start dropping packets, and some of the connecting clients would fail to fully synchronize. (#3350)
22+
- Fixed issue when using a distributed authority network topology and scene management was disabled clients would not be able to spawn any new network prefab instances until synchronization was complete. (#3350)
2123
- Fixed issue where the `MaximumInterpolationTime` could not be modified from within the inspector view or runtime. (#3337)
2224
- Fixed `ChangeOwnership` changing ownership to clients that are not observers. This also happened with automated object distribution. (#3323)
2325
- Fixed issue where `AnticipatedNetworkVariable` previous value returned by `AnticipatedNetworkVariable.OnAuthoritativeValueChanged` is updated correctly on the non-authoritative side. (#3306)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public void Handle(ref NetworkContext context)
7373
/// DANGO-TODO: Determine if this needs to be removed once the service handles object distribution
7474
networkManager.RedistributeToClients = true;
7575
networkManager.ClientsToRedistribute.Add(ClientId);
76+
77+
// TODO: We need a client synchronized message or something like that here
7678
}
7779
}
7880
}

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ public void Handle(ref NetworkContext context)
270270
// Only if scene management is disabled do we handle NetworkObject synchronization at this point
271271
if (!networkManager.NetworkConfig.EnableSceneManagement)
272272
{
273+
/// Mark the client being connected before running through the spawning synchronization so we
274+
/// can assure that if a user attempts to spawn something when an already spawned NetworkObject
275+
/// is spawned (during the initial synchronization just below) it will not error out complaining
276+
/// about the player not being connected.
277+
/// The check for this is done within <see cref="NetworkObject.SpawnInternal(bool, ulong, bool)"/>
278+
networkManager.IsConnectedClient = true;
279+
273280
// DANGO-TODO: This is a temporary fix for no DA CMB scene event handling.
274281
// We will either use this same concept or provide some way for the CMB state plugin to handle it.
275282
if (networkManager.DistributedAuthorityMode && networkManager.LocalClient.IsSessionOwner)
@@ -292,9 +299,6 @@ public void Handle(ref NetworkContext context)
292299
NetworkObject.AddSceneObject(sceneObject, m_ReceivedSceneObjectData, networkManager);
293300
}
294301

295-
// Mark the client being connected
296-
networkManager.IsConnectedClient = true;
297-
298302
if (networkManager.AutoSpawnPlayerPrefabClientSide)
299303
{
300304
networkManager.ConnectionManager.CreateAndSpawnPlayer(OwnerClientId);
@@ -315,14 +319,14 @@ public void Handle(ref NetworkContext context)
315319
if (networkManager.DistributedAuthorityMode && networkManager.CMBServiceConnection && networkManager.LocalClient.IsSessionOwner && networkManager.NetworkConfig.EnableSceneManagement)
316320
{
317321
// Mark the client being connected
318-
networkManager.IsConnectedClient = true;
322+
networkManager.IsConnectedClient = networkManager.ConnectionManager.LocalClient.IsApproved;
319323

320324
networkManager.SceneManager.IsRestoringSession = GetIsSessionRestor();
321325

322326
if (!networkManager.SceneManager.IsRestoringSession)
323327
{
324328
// Synchronize the service with the initial session owner's loaded scenes and spawned objects
325-
networkManager.SceneManager.SynchronizeNetworkObjects(NetworkManager.ServerClientId);
329+
networkManager.SceneManager.SynchronizeNetworkObjects(NetworkManager.ServerClientId, true);
326330

327331
// Spawn any in-scene placed NetworkObjects
328332
networkManager.SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
@@ -334,9 +338,9 @@ public void Handle(ref NetworkContext context)
334338
}
335339

336340
// Synchronize the service with the initial session owner's loaded scenes and spawned objects
337-
networkManager.SceneManager.SynchronizeNetworkObjects(NetworkManager.ServerClientId);
341+
networkManager.SceneManager.SynchronizeNetworkObjects(NetworkManager.ServerClientId, true);
338342

339-
// With scene management enabled and since the session owner doesn't send a Synchronize scene event synchronize itself,
343+
// With scene management enabled and since the session owner doesn't send a scene event synchronize to itself,
340344
// we need to notify the session owner that everything should be synchronized/spawned at this time.
341345
networkManager.SpawnManager.NotifyNetworkObjectsSynchronized();
342346

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1986,6 +1986,14 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene)
19861986
/// </summary>
19871987
internal Func<Scene, bool> ExcludeSceneFromSychronization;
19881988

1989+
/// <summary>
1990+
/// This is used for distributed authority sessions only and assures that
1991+
/// when many clients attempt to connect at the same time they will be
1992+
/// handled sequentially so as to not saturate the session owner's maximum
1993+
/// reliable messages.
1994+
/// </summary>
1995+
internal List<ulong> ClientConnectionQueue = new List<ulong>();
1996+
19891997
/// <summary>
19901998
/// Server Side:
19911999
/// This is used for players that have just had their connection approved and will assure they are synchronized
@@ -1994,8 +2002,30 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene)
19942002
/// synchronized.
19952003
/// </summary>
19962004
/// <param name="clientId">newly joined client identifier</param>
1997-
internal void SynchronizeNetworkObjects(ulong clientId)
2005+
/// <param name="synchronizingService">true only when invoked on a newly connected and approved client.</param>
2006+
internal void SynchronizeNetworkObjects(ulong clientId, bool synchronizingService = false)
19982007
{
2008+
// If we are connected to a live service hosted session and we are not doing the initial synchronization for the service...
2009+
if (NetworkManager.CMBServiceConnection && !synchronizingService)
2010+
{
2011+
// then as long as this is a newly connecting client add it to the connecting client queue.
2012+
// Otherwise, if this is not a newly connecting client (i.e. it is already in the queue), then go ahead and synchronize
2013+
// that client.
2014+
if (!ClientConnectionQueue.Contains(clientId))
2015+
{
2016+
ClientConnectionQueue.Add(clientId);
2017+
// If we are already synchronizing one or more clients, exit early. This client will be synchronized later.
2018+
if (ClientConnectionQueue.Count > 1)
2019+
{
2020+
if (NetworkManager.LogLevel <= LogLevel.Developer)
2021+
{
2022+
Debug.Log($"Deferring Client-{clientId} synchrnization.");
2023+
}
2024+
return;
2025+
}
2026+
}
2027+
}
2028+
19992029
// Update the clients
20002030
NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId);
20012031

@@ -2623,6 +2653,44 @@ private void HandleSessionOwnerEvent(uint sceneEventId, ulong clientId)
26232653
// DANGO-EXP TODO: Remove this once service distributes objects
26242654
NetworkManager.SpawnManager.DistributeNetworkObjects(clientId);
26252655
EndSceneEvent(sceneEventId);
2656+
2657+
// Exit early if not a distributed authority session or this is a DAHost
2658+
// (DAHost has a unique connection per client, so no need to queue synchronization)
2659+
if (!NetworkManager.DistributedAuthorityMode || NetworkManager.DAHost)
2660+
{
2661+
return;
2662+
}
2663+
2664+
// Otherwise, this is a session owner that could have pending clients to synchronize
2665+
if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection)
2666+
{
2667+
// Remove the client that just synchronized
2668+
ClientConnectionQueue.Remove(clientId);
2669+
2670+
// If we have pending clients to synchronize, then make sure they are still connected
2671+
while (ClientConnectionQueue.Count > 0)
2672+
{
2673+
// If the next client is no longer connected then remove it from the list
2674+
if (!NetworkManager.ConnectedClientsIds.Contains(ClientConnectionQueue[0]))
2675+
{
2676+
ClientConnectionQueue.RemoveAt(0);
2677+
}
2678+
else
2679+
{
2680+
break;
2681+
}
2682+
}
2683+
2684+
// If we still have any pending clients waiting, then synchronize the next one
2685+
if (ClientConnectionQueue.Count > 0)
2686+
{
2687+
if (NetworkManager.LogLevel <= LogLevel.Developer)
2688+
{
2689+
Debug.Log($"Synchronizing Client-{ClientConnectionQueue[0]}...");
2690+
}
2691+
SynchronizeNetworkObjects(ClientConnectionQueue[0]);
2692+
}
2693+
}
26262694
break;
26272695
}
26282696
default:

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,7 +1815,7 @@ internal void Shutdown()
18151815
/// </summary>
18161816
/// <param name="objectByTypeAndOwner">the table to populate</param>
18171817
/// <param name="objectTypeCount">the total number of the specific object type to distribute</param>
1818-
internal void GetObjectDistribution(ref Dictionary<uint, Dictionary<ulong, List<NetworkObject>>> objectByTypeAndOwner, ref Dictionary<uint, int> objectTypeCount)
1818+
internal void GetObjectDistribution(ulong clientId, ref Dictionary<uint, Dictionary<ulong, List<NetworkObject>>> objectByTypeAndOwner, ref Dictionary<uint, int> objectTypeCount)
18191819
{
18201820
// DANGO-TODO-MVP: Remove this once the service handles object distribution
18211821
var onlyIncludeOwnedObjects = NetworkManager.CMBServiceConnection;
@@ -1844,6 +1844,11 @@ internal void GetObjectDistribution(ref Dictionary<uint, Dictionary<ulong, List<
18441844
// At this point we only allow things marked with the distributable permission and is not locked to be distributed
18451845
if (networkObject.IsOwnershipDistributable && !networkObject.IsOwnershipLocked)
18461846
{
1847+
// Don't include anything that is not visible to the new client
1848+
if (!networkObject.Observers.Contains(clientId))
1849+
{
1850+
continue;
1851+
}
18471852

18481853
// We have to check if it is an in-scene placed NetworkObject and if it is get the source prefab asset GlobalObjectIdHash value of the in-scene placed instance
18491854
// since all in-scene placed instances use unique GlobalObjectIdHash values.
@@ -1913,7 +1918,7 @@ internal void DistributeNetworkObjects(ulong clientId)
19131918
var objectTypeCount = new Dictionary<uint, int>();
19141919

19151920
// Get all spawned objects by type and then by client owner that are spawned and can be distributed
1916-
GetObjectDistribution(ref distributedNetworkObjects, ref objectTypeCount);
1921+
GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount);
19171922

19181923
var clientCount = NetworkManager.ConnectedClientsIds.Count;
19191924

@@ -1951,7 +1956,6 @@ internal void DistributeNetworkObjects(ulong clientId)
19511956

19521957
var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1);
19531958
var distributed = 0;
1954-
19551959
// For now when we have more players then distributed NetworkObjects that
19561960
// a specific client owns, just assign half of the NetworkObjects to the new client
19571961
var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1);
@@ -1964,11 +1968,6 @@ internal void DistributeNetworkObjects(ulong clientId)
19641968
{
19651969
if ((i % offsetCount) == 0)
19661970
{
1967-
while (!ownerList.Value[i].Observers.Contains(clientId))
1968-
{
1969-
i++;
1970-
}
1971-
19721971
var children = ownerList.Value[i].GetComponentsInChildren<NetworkObject>();
19731972
// Since the ownerList.Value[i] has to be distributable, then transfer all child NetworkObjects
19741973
// with the same owner clientId and are marked as distributable also to the same client to keep
@@ -2011,7 +2010,7 @@ internal void DistributeNetworkObjects(ulong clientId)
20112010
var builder = new StringBuilder();
20122011
distributedNetworkObjects.Clear();
20132012
objectTypeCount.Clear();
2014-
GetObjectDistribution(ref distributedNetworkObjects, ref objectTypeCount);
2013+
GetObjectDistribution(clientId, ref distributedNetworkObjects, ref objectTypeCount);
20152014
builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)");
20162015
// Cycle through each prefab type
20172016
foreach (var objectTypeEntry in distributedNetworkObjects)
@@ -2026,7 +2025,6 @@ internal void DistributeNetworkObjects(ulong clientId)
20262025
}
20272026
Debug.Log(builder.ToString());
20282027
}
2029-
20302028
}
20312029

20322030
internal struct DeferredDespawnObject

0 commit comments

Comments
 (0)