Skip to content

fix: In-Scene Placed Object Parenting and Serialization Order (back port) #3388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Fixed issue where in-scene placed `NetworkObjects` could fail to synchronize its transform properly (especially without a `NetworkTransform`) if their parenting changes from the default when the scene is loaded and if the same scene remains loaded between network sessions while the parenting is completely different from the original hierarchy. (#3388)
- Fixed an issue in `UnityTransport` where the transport would accept sends on invalid connections, leading to a useless memory allocation and confusing error message. (#3383)
- Fixed issue where `NetworkAnimator` would log an error if there was no destination transition information. (#3384)
- Fixed initial `NetworkTransform` spawn, ensure it uses world space. (#3361)
- Fixed issue where `AnticipatedNetworkVariable` previous value returned by `AnticipatedNetworkVariable.OnAuthoritativeValueChanged` is updated correctly on the non-authoritative side. (#3322)

### Changed

- Changed the scene loading event serialization order for in-scene placed `NetworkObject`s to be based on their parent-child hierarchy. (#3388)

## [1.12.2] - 2025-01-17

Expand Down
75 changes: 37 additions & 38 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,18 @@ private bool IsEditingPrefab()
/// </remarks>
private void CheckForInScenePlaced()
{
if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
if (gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
{
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
var assetPath = AssetDatabase.GetAssetPath(prefab);
var sourceAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(assetPath);
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
if (PrefabUtility.IsPartOfAnyPrefab(this))
{
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
EditorUtility.SetDirty(this);
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
var assetPath = AssetDatabase.GetAssetPath(prefab);
var sourceAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(assetPath);
if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash)
{
InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash;
EditorUtility.SetDirty(this);
}
}
IsSceneObject = true;
}
Expand Down Expand Up @@ -1241,7 +1244,7 @@ private void OnTransformParentChanged()
// we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one.
internal static HashSet<NetworkObject> OrphanChildren = new HashSet<NetworkObject>();

internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false)
internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false, bool enableNotification = true)
{
if (!AutoObjectParentSync)
{
Expand Down Expand Up @@ -1314,7 +1317,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa
// to WorldPositionStays which can cause scaling issues if the parent's
// scale is not the default (Vetctor3.one) value.
transform.SetParent(null, m_CachedWorldPositionStays);
InvokeBehaviourOnNetworkObjectParentChanged(null);
if (enableNotification)
{
InvokeBehaviourOnNetworkObjectParentChanged(null);
}
return true;
}

Expand All @@ -1340,7 +1346,10 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa

m_CachedParent = parentObject.transform;
transform.SetParent(parentObject.transform, m_CachedWorldPositionStays);
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
if (enableNotification)
{
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
}
return true;
}

Expand Down Expand Up @@ -1819,6 +1828,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
{
var obj = new SceneObject
{
HasParent = transform.parent != null,
WorldPositionStays = m_CachedWorldPositionStays,
NetworkObjectId = NetworkObjectId,
OwnerClientId = OwnerClientId,
IsPlayerObject = IsPlayerObject,
Expand All @@ -1829,31 +1840,16 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
TargetClientId = targetClientId
};

NetworkObject parentNetworkObject = null;

if (!AlwaysReplicateAsRoot && transform.parent != null)
// Handle Parenting
if (!AlwaysReplicateAsRoot && obj.HasParent)
{
parentNetworkObject = transform.parent.GetComponent<NetworkObject>();
// In-scene placed NetworkObjects parented under GameObjects with no NetworkObject
// should set the has parent flag and preserve the world position stays value
if (parentNetworkObject == null && obj.IsSceneObject)
{
obj.HasParent = true;
obj.WorldPositionStays = m_CachedWorldPositionStays;
}
}
var parentNetworkObject = transform.parent.GetComponent<NetworkObject>();

if (parentNetworkObject != null)
{
obj.HasParent = true;
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
obj.WorldPositionStays = m_CachedWorldPositionStays;
var latestParent = GetNetworkParenting();
var isLatestParentSet = latestParent != null && latestParent.HasValue;
obj.IsLatestParentSet = isLatestParentSet;
if (isLatestParentSet)
if (parentNetworkObject)
{
obj.LatestParent = latestParent.Value;
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
obj.LatestParent = GetNetworkParenting();
obj.IsLatestParentSet = obj.LatestParent != null && obj.LatestParent.HasValue;
}
}

Expand All @@ -1866,12 +1862,6 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId)
var syncRotationPositionLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays;
var syncScaleLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays;

// Always synchronize in-scene placed object's scale using local space
if (obj.IsSceneObject)
{
syncScaleLocalSpaceRelative = obj.HasParent;
}

// If auto object synchronization is turned off
if (!AutoObjectParentSync)
{
Expand Down Expand Up @@ -1949,6 +1939,15 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
var bufferSerializer = new BufferSerializer<BufferSerializerReader>(new BufferSerializerReader(reader));
networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId);

// If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are
// being told we do not have a parent, then we want to clear the latest parent so it is not automatically
// "re-parented" to the original parent. This can happen if not unloading the scene and the parenting of
// the in-scene placed Networkobject changes several times over different sessions.
if (sceneObject.IsSceneObject && !sceneObject.HasParent && networkObject.m_LatestParent.HasValue)
{
networkObject.m_LatestParent = null;
}

// Spawn the NetworkObject
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,14 @@ internal void AddSpawnedNetworkObjects()
m_NetworkObjectsSync.Add(sobj);
}
}
SortObjectsToSync();
}

/// <summary>
/// Used to order the object serialization for both synchronization and scene loading
/// </summary>
private void SortObjectsToSync()
{
// Sort by INetworkPrefabInstanceHandler implementation before the
// NetworkObjects spawned by the implementation
m_NetworkObjectsSync.Sort(SortNetworkObjects);
Expand Down Expand Up @@ -568,20 +575,31 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer)
// Write our count place holder (must not be packed!)
writer.WriteValueSafe((ushort)0);

// Clear our objects to sync and build a list of the in-scene placed NetworkObjects instantiated and spawned locally
m_NetworkObjectsSync.Clear();
foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects)
{
foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value)
{
if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId))
{
// Serialize the NetworkObject
var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId);
sceneObject.Serialize(writer);
numberOfObjects++;
m_NetworkObjectsSync.Add(keyValuePairBySceneHandle.Value);
}
}
}

// Sort the objects to sync based on parenting hierarchy
SortObjectsToSync();

// Serialize the sorted objects to sync.
foreach (var objectToSycn in m_NetworkObjectsSync)
{
// Serialize the NetworkObject
var sceneObject = objectToSycn.GetMessageSceneObject(TargetClientId);
sceneObject.Serialize(writer);
numberOfObjects++;
}

// Write the number of despawned in-scene placed NetworkObjects
writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count);
// Write the scene handle and GlobalObjectIdHash value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default;
var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default;
var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays;
var isSpawnedByPrefabHandler = false;

// If scene management is disabled or the NetworkObject was dynamically spawned
if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject)
Expand Down Expand Up @@ -605,33 +604,40 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;


var nonNetworkObjectParent = false;
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
// This is a special case scenario where a late joining client has joined and loaded one or
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
// synchronization information does not indicate the NetworkObject in question has a parent.
// Under this scenario, we want to remove the parent before spawning and setting the transform values.
// synchronization information does not indicate the NetworkObject in question has a parent = or =
// the parent has changed.
// For this we will want to remove the parent before spawning and setting the transform values based
// on several possible scenarios.
if (sceneObject.IsSceneObject && networkObject.transform.parent != null)
{
var parentNetworkObject = networkObject.transform.parent.GetComponent<NetworkObject>();
// if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not
// include parenting, then we need to force the removal of that parent
if (!sceneObject.HasParent && parentNetworkObject)
{
// remove the parent
networkObject.ApplyNetworkParenting(true, true);
}
else if (sceneObject.HasParent && !parentNetworkObject)
// special case to handle being parented under a GameObject with no NetworkObject
nonNetworkObjectParent = !parentNetworkObject && sceneObject.HasParent;

// If the in-scene placed NetworkObject has a parent NetworkObject...
if (parentNetworkObject)
{
nonNetworkObjectParent = true;
// Then remove the parent only if:
// - The authority says we don't have a parent (but locally we do).
// - The auhtority says we have a parent but either of the two are true:
// -- It isn't the same parent.
// -- It was parented using world position stays.
if (!sceneObject.HasParent || (sceneObject.IsLatestParentSet
&& (sceneObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || sceneObject.WorldPositionStays)))
{
// If parenting without notifications then we are temporarily removing the parent to set the transform
// values before reparenting under the current parent.
networkObject.ApplyNetworkParenting(true, true, enableNotification: !sceneObject.HasParent);
}
}
}

// Set the transform unless we were spawned by a prefab handler
// Note: prefab handlers are provided the position and rotation
// but it is up to the user to set those values
if (sceneObject.HasTransform && !isSpawnedByPrefabHandler)
// Set the transform only if the sceneObject includes transform information.
if (sceneObject.HasTransform)
{
// If world position stays is true or we have auto object parent synchronization disabled
// then we want to apply the position and rotation values world space relative
Expand Down Expand Up @@ -674,7 +680,6 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
networkObject.SetNetworkParenting(parentId, worldPositionStays);
}


// Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL
// until the scene is loaded. They are then migrated back into the newly loaded and currently active scene.
if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad)
Expand Down