diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 228a4217ea..99197d550f 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -15,6 +15,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Fixed
+- Fixed issue where the `NetworkObjectIdHash` value could be incorrect when entering play mode while still in prefab edit mode with pending changes and using MPPM. (#3162)
- Fixed issue where a sever only `NetworkManager` instance would spawn the actual `NetworkPrefab`'s `GameObject` as opposed to creating an instance of it. (#3160)
- Fixed issue where only the session owner (as opposed to all clients) would handle spawning prefab overrides properly when using a distributed authority network topology. (#3160)
- Fixed issue where an exception was thrown when calling `NetworkManager.Shutdown` after calling `UnityTransport.Shutdown`. (#3118)
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index eacda7e87d..6a7d71f159 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -1023,8 +1023,7 @@ private void ModeChanged(PlayModeStateChange change)
{
if (IsListening && change == PlayModeStateChange.ExitingPlayMode)
{
- // Make sure we are not holding onto anything in case domain reload is disabled
- ShutdownInternal();
+ OnApplicationQuit();
}
}
#endif
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 501cda138d..5d94f4fd3c 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -102,6 +102,16 @@ public uint PrefabIdHash
private const int k_SceneObjectType = 2;
private const int k_SourceAssetObjectType = 3;
+ // Used to track any InContext or InIsolation prefab being edited.
+ private static PrefabStage s_PrefabStage;
+ // The network prefab asset that the edit mode scene has created an instance of (s_PrefabInstance).
+ private static NetworkObject s_PrefabAsset;
+ // The InContext or InIsolation edit mode network prefab scene instance of the prefab asset (s_PrefabAsset).
+ private static NetworkObject s_PrefabInstance;
+
+ private static bool s_DebugPrefabIdGeneration;
+
+
[ContextMenu("Refresh In-Scene Prefab Instances")]
internal void RefreshAllPrefabInstances()
{
@@ -134,25 +144,119 @@ internal void RefreshAllPrefabInstances()
NetworkObjectRefreshTool.ProcessScenes();
}
+ ///
+ /// Register for opened and closing event notifications.
+ ///
+ [InitializeOnLoadMethod]
+ private static void OnApplicationStart()
+ {
+ PrefabStage.prefabStageOpened -= PrefabStageOpened;
+ PrefabStage.prefabStageOpened += PrefabStageOpened;
+ PrefabStage.prefabStageClosing -= PrefabStageClosing;
+ PrefabStage.prefabStageClosing += PrefabStageClosing;
+ }
+
+ private static void PrefabStageClosing(PrefabStage prefabStage)
+ {
+ // If domain reloading is enabled, then this will be null when we return from playmode.
+ if (s_PrefabStage == null)
+ {
+ // Determine if we have a network prefab opened in edit mode or not.
+ CheckPrefabStage(prefabStage);
+ }
+
+ s_PrefabStage = null;
+ s_PrefabInstance = null;
+ s_PrefabAsset = null;
+ }
+
+ private static void PrefabStageOpened(PrefabStage prefabStage)
+ {
+ // Determine if we have a network prefab opened in edit mode or not.
+ CheckPrefabStage(prefabStage);
+ }
+
+ ///
+ /// Determines if we have opened a network prefab in edit mode (InContext or InIsolation)
+ ///
+ ///
+ /// InContext: Typically means a are in prefab edit mode for an in-scene placed network prefab instance.
+ /// (currently no such thing as a network prefab with nested network prefab instances)
+ ///
+ /// InIsolation: Typically means we are in prefb edit mode for a prefab asset.
+ ///
+ ///
+ private static void CheckPrefabStage(PrefabStage prefabStage)
+ {
+ s_PrefabStage = prefabStage;
+ s_PrefabInstance = prefabStage.prefabContentsRoot?.GetComponent();
+ if (s_PrefabInstance)
+ {
+ // We acquire the source prefab that the prefab edit mode scene instance was instantiated from differently for InContext than InSolation.
+ if (s_PrefabStage.mode == PrefabStage.Mode.InContext && s_PrefabStage.openedFromInstanceRoot != null)
+ {
+ // This is needed to handle the scenario where a user completely loads a new scene while in an InContext prefab edit mode.
+ try
+ {
+ s_PrefabAsset = s_PrefabStage.openedFromInstanceRoot?.GetComponent();
+ }
+ catch
+ {
+ s_PrefabAsset = null;
+ }
+ }
+ else
+ {
+ // When editing in InIsolation mode, load the original prefab asset from the provided path.
+ s_PrefabAsset = AssetDatabase.LoadAssetAtPath(s_PrefabStage.assetPath);
+ }
+
+ if (s_PrefabInstance.GlobalObjectIdHash != s_PrefabAsset.GlobalObjectIdHash)
+ {
+ s_PrefabInstance.GlobalObjectIdHash = s_PrefabAsset.GlobalObjectIdHash;
+ // For InContext mode, we don't want to record these modifications (the in-scene GlobalObjectIdHash is serialized with the scene).
+ if (s_PrefabStage.mode == PrefabStage.Mode.InIsolation)
+ {
+ PrefabUtility.RecordPrefabInstancePropertyModifications(s_PrefabAsset);
+ }
+ }
+ }
+ else
+ {
+ s_PrefabStage = null;
+ s_PrefabInstance = null;
+ s_PrefabAsset = null;
+ }
+ }
+
+ ///
+ /// GlobalObjectIdHash values are generated during validation.
+ ///
internal void OnValidate()
{
- // do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode
+ // Always exit early if we are in prefab edit mode and this instance is the
+ // prefab instance within the InContext or InIsolation edit scene.
+ if (s_PrefabInstance == this)
+ {
+ return;
+ }
+
+ // Do not regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in play mode.
if (EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
{
return;
}
- // do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode
+ // Do not regenerate GlobalObjectIdHash if Editor is transitioning into or out of play mode.
if (!EditorApplication.isPlaying && EditorApplication.isPlayingOrWillChangePlaymode)
{
return;
}
- // Get a global object identifier for this network prefab
- var globalId = GetGlobalId();
-
+ // Get a global object identifier for this network prefab.
+ var globalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
- // if the identifier type is 0, then don't update the GlobalObjectIdHash
+ // if the identifier type is 0, then don't update the GlobalObjectIdHash.
if (globalId.identifierType == k_NullObjectType)
{
return;
@@ -161,47 +265,34 @@ internal void OnValidate()
var oldValue = GlobalObjectIdHash;
GlobalObjectIdHash = globalId.ToString().Hash32();
- // If the GlobalObjectIdHash value changed, then mark the asset dirty
+ // Always check for in-scene placed to assure any previous version scene assets with in-scene place NetworkObjects gets updated.
+ CheckForInScenePlaced();
+
+ // If the GlobalObjectIdHash value changed, then mark the asset dirty.
if (GlobalObjectIdHash != oldValue)
{
- // Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed)
- if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name)
+ // Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed).
+ if (IsSceneObject.HasValue && IsSceneObject.Value)
{
- // Sanity check to make sure this is a scene placed object
+ // Sanity check to make sure this is a scene placed object.
if (globalId.identifierType != k_SceneObjectType)
{
- // This should never happen, but in the event it does throw and error
+ // This should never happen, but in the event it does throw and error.
Debug.LogError($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}! **Report this error**");
}
- // If this is a prefab instance
+ // If this is a prefab instance, then we want to mark it as having been updated in order for the udpated GlobalObjectIdHash value to be saved.
if (PrefabUtility.IsPartOfAnyPrefab(this))
{
- // We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty)
+ // We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty).
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
}
}
- else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it
+ else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it.
{
EditorUtility.SetDirty(this);
}
}
-
- // Always check for in-scene placed to assure any previous version scene assets with in-scene place NetworkObjects gets updated
- CheckForInScenePlaced();
- }
-
- private bool IsEditingPrefab()
- {
- // Check if we are directly editing the prefab
- var stage = PrefabStageUtility.GetPrefabStage(gameObject);
-
- // if we are not editing the prefab directly (or a sub-prefab), then return the object identifier
- if (stage == null || stage.assetPath == null)
- {
- return false;
- }
- return true;
}
///
@@ -212,13 +303,12 @@ private bool IsEditingPrefab()
///
/// This NetworkObject is considered an in-scene placed prefab asset instance if it is:
/// - Part of a prefab
- /// - Not being directly edited
/// - Within a valid scene that is part of the scenes in build list
/// (In-scene defined NetworkObjects that are not part of a prefab instance are excluded.)
///
private void CheckForInScenePlaced()
{
- if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
+ if (PrefabUtility.IsPartOfAnyPrefab(this) && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
{
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
var assetPath = AssetDatabase.GetAssetPath(prefab);
@@ -231,55 +321,6 @@ private void CheckForInScenePlaced()
IsSceneObject = true;
}
}
-
- private GlobalObjectId GetGlobalId()
- {
- var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
-
- // If not editing a prefab, then just use the generated id
- if (!IsEditingPrefab())
- {
- return instanceGlobalId;
- }
-
- // If the asset doesn't exist at the given path, then return the object identifier
- var prefabStageAssetPath = PrefabStageUtility.GetPrefabStage(gameObject).assetPath;
- // If (for some reason) the asset path is null return the generated id
- if (prefabStageAssetPath == null)
- {
- return instanceGlobalId;
- }
-
- var theAsset = AssetDatabase.LoadAssetAtPath(prefabStageAssetPath);
- // If there is no asset at that path (for some odd/edge case reason), return the generated id
- if (theAsset == null)
- {
- return instanceGlobalId;
- }
-
- // If we can't get the asset GUID and/or the file identifier, then return the object identifier
- if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(theAsset, out var guid, out long localFileId))
- {
- return instanceGlobalId;
- }
-
- // Note: If we reached this point, then we are most likely opening a prefab to edit.
- // The instanceGlobalId will be constructed as if it is a scene object, however when it
- // is serialized its value will be treated as a file asset (the "why" to the below code).
-
- // Construct an imported asset identifier with the type being a source asset object type
- var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, k_SourceAssetObjectType, guid, (ulong)localFileId, 0);
-
- // If we can't parse the result log an error and return the instanceGlobalId
- if (!GlobalObjectId.TryParse(prefabGlobalIdText, out var prefabGlobalId))
- {
- Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})! ** Please Report This Error **");
- return instanceGlobalId;
- }
-
- // Otherwise, return the constructed identifier for the source prefab asset
- return prefabGlobalId;
- }
#endif // UNITY_EDITOR
///