Skip to content

Commit 5982716

Browse files
fix: only send scene migrations to observers, and spawn a NetworkObject shown after a migration in its authority's scene (#4185)
* fix: only send scene migrations to observers, and spawn a NetworkObject shown after a migration in the server's scene ObjectSceneChanged was sent to every connected client, including clients that did not observe the migrated NetworkObjects and had never spawned them, which logged "Trying to synchronize NetworkObjectId but it was not spawned". In client-server mode it is now only sent to clients that observe at least one migrated NetworkObject, and each client is only sent the NetworkObjects it observes. Distributed authority is unchanged. A client that starts observing a NetworkObject with NetworkShow after the NetworkObject migrated into another scene was never sent that migration, and instantiated the NetworkObject in its active scene. The client now moves a spawned NetworkObject into its server-side scene, as it already does for NetworkObjects that are part of its initial synchronization. * fix: apply the scene migration fixes in distributed authority mode Distributed authority still sent a scene migration to every client, so clients that did not observe a migrated NetworkObject logged "Trying to synchronize NetworkObjectId but it was not spawned", and a NetworkObject shown after it migrated spawned in the client's active scene. The owner now only sends a migration to the clients that observe a migrated NetworkObject, as in client-server mode. The CMB service's own copy still includes every migrated NetworkObject, and a migration the DAHost forwards is not filtered again. A NetworkObject a distributed authority client spawns when it is shown is moved into its owner's scene. NetworkObjectSceneMigrationObserverTests now also runs as DAHost. * chore: correct the comment on when the CMB service is sent a scene migration Only the session owner sends the CMB service its own copy of a migration; a client that is not the session owner does not tell the service about objects only it observes. * Implement UseCMBService method for Rust server Added method to disable CMB service for Rust server compatibility. * Fix XML summary comment formatting in test file * Fix summary comment formatting in test file
1 parent fdadb7d commit 5982716

6 files changed

Lines changed: 279 additions & 25 deletions

File tree

‎com.unity.netcode.gameobjects/CHANGELOG.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ Additional documentation and release notes are available at [Multiplayer Documen
2222

2323
### Fixed
2424

25-
- Fixed an issue where mixed authority nested `NetworkTransform` instances could stop child/nested instances from updating because an authoritative `NetworkTransform` (root or otherwise) would remove the `NetworkObject` from the update group, preventing non-authoritative instances from updating their state on the authority side. (#4169)
25+
- Issue where moving a `NetworkObject` into another scene made the clients that did not observe it log "Trying to synchronize NetworkObjectId but it was not spawned". The scene migration is now only sent to the clients that observe the `NetworkObject`. (#4185)
26+
- Issue where a `NetworkObject` that was moved into another scene while hidden from a client spawned in that client's active scene when it was shown with `NetworkShow`, instead of the scene it is in on the server. (#4185)
27+
- Issue where mixed authority nested `NetworkTransform` instances could stop child/nested instances from updating because an authoritative `NetworkTransform` (root or otherwise) would remove the `NetworkObject` from the update group, preventing non-authoritative instances from updating their state on the authority side. (#4169)
2628

2729
### Security
2830

‎com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende
178178
networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
179179
return;
180180
}
181+
182+
// A NetworkObject shown to this client after it migrated into another scene was instantiated in the
183+
// active scene, and the migration was only sent to the clients observing it at the time.
184+
if (networkManager.NetworkConfig.EnableSceneManagement)
185+
{
186+
networkManager.SceneManager.SynchronizeNetworkObjectScene(networkObject);
187+
}
181188
}
182189
else
183190
{
@@ -206,6 +213,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende
206213
networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
207214
return;
208215
}
216+
217+
// Same as client-server: the migration was only sent to the clients observing it at the time
218+
if (networkManager.NetworkConfig.EnableSceneManagement)
219+
{
220+
networkManager.SceneManager.SynchronizeNetworkObjectScene(networkObject);
221+
}
209222
}
210223

211224
// DA - NGO CMB SERVICE NOTES:

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

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,27 +2253,39 @@ private void SynchronizeNetworkObjectScene()
22532253
{
22542254
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
22552255
{
2256-
// This is only done for dynamically spawned NetworkObjects
2257-
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
2258-
// then skip it (it will reside in the currently active scene in this scenario on the client-side)
2259-
if (!networkObject.InScenePlaced && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
2260-
{
2261-
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
2256+
SynchronizeNetworkObjectScene(networkObject);
2257+
}
2258+
}
22622259

2263-
// If the NetworkObject does not have a parent and is not in the same scene as it is on the server side, then find the right scene
2264-
// and move it to that scene.
2265-
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
2260+
/// <summary>
2261+
/// Migrates a single client-side dynamically spawned NetworkObject into the scene it is in on the server-side.
2262+
/// </summary>
2263+
/// <remarks>
2264+
/// Also used for hybrid prefab instances that are part of the initial synchronization but only spawn once
2265+
/// their ghost arrives, which can be after the synchronization has completed.
2266+
/// </remarks>
2267+
internal void SynchronizeNetworkObjectScene(NetworkObject networkObject)
2268+
{
2269+
// This is only done for dynamically spawned NetworkObjects
2270+
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
2271+
// then skip it (it will reside in the currently active scene in this scenario on the client-side)
2272+
if (!networkObject.InScenePlaced && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
2273+
{
2274+
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
2275+
2276+
// If the NetworkObject does not have a parent and is not in the same scene as it is on the server side, then find the right scene
2277+
// and move it to that scene.
2278+
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
2279+
{
2280+
if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
22662281
{
2267-
if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
2268-
{
2269-
var scene = ScenesLoaded[networkObject.SceneOriginHandle];
2270-
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
2271-
}
2272-
else if (NetworkManager.LogLevel <= LogLevel.Normal)
2273-
{
2274-
NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
2275-
$"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
2276-
}
2282+
var scene = ScenesLoaded[networkObject.SceneOriginHandle];
2283+
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
2284+
}
2285+
else if (NetworkManager.LogLevel <= LogLevel.Normal)
2286+
{
2287+
NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
2288+
$"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
22772289
}
22782290
}
22792291
}
@@ -2992,6 +3004,26 @@ internal void MigrateNetworkObjectsIntoScenes()
29923004

29933005

29943006
private List<NetworkSceneHandle> m_ScenesToRemoveFromObjectMigration = new();
3007+
private List<ulong> m_SceneMigrationTargetIds = new();
3008+
3009+
private bool ObservesMigratedNetworkObject(ulong clientId)
3010+
{
3011+
foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
3012+
{
3013+
if (!sceneEntry.Value.TryGetValue(NetworkManager.LocalClientId, out var migratedObjects))
3014+
{
3015+
continue;
3016+
}
3017+
foreach (var networkObject in migratedObjects)
3018+
{
3019+
if (networkObject.Observers.Contains(clientId))
3020+
{
3021+
return true;
3022+
}
3023+
}
3024+
}
3025+
return false;
3026+
}
29953027

29963028
/// <summary>
29973029
/// Should be invoked during PostLateUpdate just prior to the NetworkMessageManager processes its outbound message queue.
@@ -3056,13 +3088,30 @@ internal void CheckForAndSendNetworkObjectSceneChanged()
30563088
return;
30573089
}
30583090

3091+
// Only send to the clients that observe at least one of the migrated NetworkObjects
3092+
m_SceneMigrationTargetIds.Clear();
3093+
foreach (var clientId in NetworkManager.ConnectedClientsIds)
3094+
{
3095+
if (clientId != NetworkManager.LocalClientId && ObservesMigratedNetworkObject(clientId))
3096+
{
3097+
m_SceneMigrationTargetIds.Add(clientId);
3098+
}
3099+
}
3100+
3101+
// The session owner still sends the CMB service its copy when no other client observes the migrated objects
3102+
if (m_SceneMigrationTargetIds.Count == 0 && !NetworkManager.CMBServiceConnection)
3103+
{
3104+
ObjectsMigratedIntoNewScene.Clear();
3105+
return;
3106+
}
3107+
30593108
// Some NetworkObjects still exist, send the message
30603109
var sceneEvent = BeginSceneEvent();
30613110
sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged;
30623111
// SendSceneEventData can throw an exception. We need to wrap this and recover from the exception gracefully.
30633112
try
30643113
{
3065-
SendSceneEventData(sceneEvent.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.LocalClientId).ToArray());
3114+
SendSceneEventData(sceneEvent.SceneEventId, m_SceneMigrationTargetIds.ToArray());
30663115
}
30673116
catch (Exception ex)
30683117
{

‎com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,18 @@ private void ReadSceneEventProgressDone(FastBufferReader reader)
12011201
internal bool IsForwarding;
12021202
private ulong m_OwnerId;
12031203

1204+
/// <summary>
1205+
/// Whether a migrated <see cref="NetworkObject"/> is sent to the target client: only when the target observes it.
1206+
/// </summary>
1207+
/// <remarks>
1208+
/// The CMB service keeps the session state, so its own copy includes every migrated NetworkObject.
1209+
/// A forwarded migration was already filtered by the owner that sent it.
1210+
/// </remarks>
1211+
private bool IsMigrationSentToTarget(NetworkObject networkObject)
1212+
{
1213+
return IsForwarding || (m_NetworkManager.CMBServiceConnection && TargetClientId == NetworkManager.ServerClientId) || networkObject.Observers.Contains(TargetClientId);
1214+
}
1215+
12041216
private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer)
12051217
{
12061218
var sceneManager = m_NetworkManager.SceneManager;
@@ -1224,17 +1236,35 @@ private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer)
12241236
{
12251237
// Since these are separated by scene then owner, there could be scenes that have
12261238
// no changes.
1227-
if (!sceneHandleObjects.Value.ContainsKey(networkManagerClientId))
1239+
if (!sceneHandleObjects.Value.TryGetValue(networkManagerClientId, out var migratedObjects))
1240+
{
1241+
continue;
1242+
}
1243+
1244+
// A client is only told about the objects it observes, since it has not spawned the others.
1245+
var objectCount = 0;
1246+
foreach (var networkObject in migratedObjects)
1247+
{
1248+
if (IsMigrationSentToTarget(networkObject))
1249+
{
1250+
objectCount++;
1251+
}
1252+
}
1253+
if (objectCount == 0)
12281254
{
12291255
continue;
12301256
}
1257+
12311258
// Write the scene handle
12321259
writer.WriteValueSafe(sceneHandleObjects.Key);
12331260
// Write the number of NetworkObjectIds to expect
1234-
writer.WriteValueSafe(sceneHandleObjects.Value[networkManagerClientId].Count);
1235-
foreach (var networkObject in sceneHandleObjects.Value[networkManagerClientId])
1261+
writer.WriteValueSafe(objectCount);
1262+
foreach (var networkObject in migratedObjects)
12361263
{
1237-
writer.WriteValueSafe(networkObject.NetworkObjectId);
1264+
if (IsMigrationSentToTarget(networkObject))
1265+
{
1266+
writer.WriteValueSafe(networkObject.NetworkObjectId);
1267+
}
12381268
}
12391269
entriesWritten++;
12401270
}

0 commit comments

Comments
 (0)