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