Skip to content
4 changes: 3 additions & 1 deletion com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- 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)
- 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)
- 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)
- 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)

### Security

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende
networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
return;
}

// A NetworkObject shown to this client after it migrated into another scene was instantiated in the
// active scene, and the migration was only sent to the clients observing it at the time.
if (networkManager.NetworkConfig.EnableSceneManagement)
{
networkManager.SceneManager.SynchronizeNetworkObjectScene(networkObject);
}
}
else
{
Expand Down Expand Up @@ -206,6 +213,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende
networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId));
return;
}

// Same as client-server: the migration was only sent to the clients observing it at the time
if (networkManager.NetworkConfig.EnableSceneManagement)
{
networkManager.SceneManager.SynchronizeNetworkObjectScene(networkObject);
}
}

// DA - NGO CMB SERVICE NOTES:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2253,27 +2253,39 @@
{
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
{
// This is only done for dynamically spawned NetworkObjects
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
// then skip it (it will reside in the currently active scene in this scenario on the client-side)
if (!networkObject.InScenePlaced && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
{
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];
SynchronizeNetworkObjectScene(networkObject);
}
}

// 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
// and move it to that scene.
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
/// <summary>
/// Migrates a single client-side dynamically spawned NetworkObject into the scene it is in on the server-side.
/// </summary>
/// <remarks>
/// Also used for hybrid prefab instances that are part of the initial synchronization but only spawn once
/// their ghost arrives, which can be after the synchronization has completed.
/// </remarks>
internal void SynchronizeNetworkObjectScene(NetworkObject networkObject)
{
// This is only done for dynamically spawned NetworkObjects
// Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded
// then skip it (it will reside in the currently active scene in this scenario on the client-side)
if (!networkObject.InScenePlaced && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle))
{
networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle];

// 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
// and move it to that scene.
if (networkObject.gameObject.scene.handle != networkObject.SceneOriginHandle && networkObject.transform.parent == null)
{
if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
{
if (ScenesLoaded.ContainsKey(networkObject.SceneOriginHandle))
{
var scene = ScenesLoaded[networkObject.SceneOriginHandle];
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
else if (NetworkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +
$"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
}
var scene = ScenesLoaded[networkObject.SceneOriginHandle];
SceneManager.MoveGameObjectToScene(networkObject.gameObject, scene);
}
else if (NetworkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{networkObject.gameObject.name}] Server - " +

Check warning on line 2287 in com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs#L2285-L2287

Added lines #L2285 - L2287 were not covered by tests
$"client scene mismatch detected! Client-side has no scene loaded with handle ({networkObject.SceneOriginHandle})!");
}
}
}
Expand Down Expand Up @@ -2992,6 +3004,26 @@


private List<NetworkSceneHandle> m_ScenesToRemoveFromObjectMigration = new();
private List<ulong> m_SceneMigrationTargetIds = new();

private bool ObservesMigratedNetworkObject(ulong clientId)
{
foreach (var sceneEntry in ObjectsMigratedIntoNewScene)
{
if (!sceneEntry.Value.TryGetValue(NetworkManager.LocalClientId, out var migratedObjects))
{
continue;

Check warning on line 3015 in com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs#L3014-L3015

Added lines #L3014 - L3015 were not covered by tests
}
foreach (var networkObject in migratedObjects)
{
if (networkObject.Observers.Contains(clientId))
{
return true;
}
}
}
return false;
}

/// <summary>
/// Should be invoked during PostLateUpdate just prior to the NetworkMessageManager processes its outbound message queue.
Expand Down Expand Up @@ -3056,13 +3088,30 @@
return;
}

// Only send to the clients that observe at least one of the migrated NetworkObjects
m_SceneMigrationTargetIds.Clear();
foreach (var clientId in NetworkManager.ConnectedClientsIds)
{
if (clientId != NetworkManager.LocalClientId && ObservesMigratedNetworkObject(clientId))
{
m_SceneMigrationTargetIds.Add(clientId);
}
}

// The session owner still sends the CMB service its copy when no other client observes the migrated objects
if (m_SceneMigrationTargetIds.Count == 0 && !NetworkManager.CMBServiceConnection)
Comment thread
NoelStephensUnity marked this conversation as resolved.
{
ObjectsMigratedIntoNewScene.Clear();
return;
}

// Some NetworkObjects still exist, send the message
var sceneEvent = BeginSceneEvent();
sceneEvent.SceneEventType = SceneEventType.ObjectSceneChanged;
// SendSceneEventData can throw an exception. We need to wrap this and recover from the exception gracefully.
try
{
SendSceneEventData(sceneEvent.SceneEventId, NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.LocalClientId).ToArray());
SendSceneEventData(sceneEvent.SceneEventId, m_SceneMigrationTargetIds.ToArray());
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,18 @@
internal bool IsForwarding;
private ulong m_OwnerId;

/// <summary>
/// Whether a migrated <see cref="NetworkObject"/> is sent to the target client: only when the target observes it.
/// </summary>
/// <remarks>
/// The CMB service keeps the session state, so its own copy includes every migrated NetworkObject.
/// A forwarded migration was already filtered by the owner that sent it.
/// </remarks>
private bool IsMigrationSentToTarget(NetworkObject networkObject)
{
return IsForwarding || (m_NetworkManager.CMBServiceConnection && TargetClientId == NetworkManager.ServerClientId) || networkObject.Observers.Contains(TargetClientId);
}

private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer)
{
var sceneManager = m_NetworkManager.SceneManager;
Expand All @@ -1224,17 +1236,35 @@
{
// Since these are separated by scene then owner, there could be scenes that have
// no changes.
if (!sceneHandleObjects.Value.ContainsKey(networkManagerClientId))
if (!sceneHandleObjects.Value.TryGetValue(networkManagerClientId, out var migratedObjects))
{
continue;

Check warning on line 1241 in com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs#L1240-L1241

Added lines #L1240 - L1241 were not covered by tests
}

// A client is only told about the objects it observes, since it has not spawned the others.
var objectCount = 0;
foreach (var networkObject in migratedObjects)
{
if (IsMigrationSentToTarget(networkObject))
{
objectCount++;
}
}
if (objectCount == 0)
{
continue;
}

// Write the scene handle
writer.WriteValueSafe(sceneHandleObjects.Key);
// Write the number of NetworkObjectIds to expect
writer.WriteValueSafe(sceneHandleObjects.Value[networkManagerClientId].Count);
foreach (var networkObject in sceneHandleObjects.Value[networkManagerClientId])
writer.WriteValueSafe(objectCount);
foreach (var networkObject in migratedObjects)
{
writer.WriteValueSafe(networkObject.NetworkObjectId);
if (IsMigrationSentToTarget(networkObject))
{
writer.WriteValueSafe(networkObject.NetworkObjectId);
}
}
entriesWritten++;
}
Expand Down
Loading
Loading