From 98bca33eac4dcec3a2ab8031be17deee6f9c540d Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Tue, 19 Sep 2023 18:08:35 -0500 Subject: [PATCH 01/10] update WIP to resolve MTT-7055. --- .../Runtime/Core/NetworkObject.cs | 157 +++++++++++++++++- 1 file changed, 149 insertions(+), 8 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 64bf640627..f047b998c7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -15,16 +15,98 @@ namespace Unity.Netcode { +#if UNITY_EDITOR + internal class NetworkObjectManagement + { + [InitializeOnLoadMethod] + internal static void OnLoad() + { + EditorApplication.update += OnEditorUpdate; + } + + internal enum SceneDirtyStates + { + None, + Mark, + Marked, + Save, + Saving, + Saved + } + + internal static SceneDirtyStates SceneDirtyState; + + private static float s_MarkedDelay; + + internal static Scene TargetScene; + + internal static void OnEditorUpdate() + { + if (SceneDirtyState == SceneDirtyStates.None) + { + return; + } + switch (SceneDirtyState) + { + case SceneDirtyStates.Mark: + { + if (EditorSceneManager.MarkSceneDirty(TargetScene)) + { + s_MarkedDelay = Time.realtimeSinceStartup + 0.1f; + Debug.Log($"[{TargetScene.name}] marked as dirty!"); + SceneDirtyState = SceneDirtyStates.Marked; + } + break; + } + case SceneDirtyStates.Marked: + { + if (s_MarkedDelay < Time.realtimeSinceStartup) + { + Debug.Log($"[{TargetScene.name}] Saving scene..."); + SceneDirtyState = SceneDirtyStates.Save; + } + break; + } + case SceneDirtyStates.Save: + { + s_MarkedDelay = 0.0f; + EditorSceneManager.sceneSaved += SceneSaved; + SceneDirtyState = SceneDirtyStates.Saving; + if (!EditorSceneManager.SaveScene(TargetScene)) + { + Debug.LogError($"[{TargetScene.name}] Failed to save scene!"); + } + break; + } + case SceneDirtyStates.Saved: + { + Debug.Log($"[{TargetScene.name}] Scene saved!"); + SceneDirtyState = SceneDirtyStates.None; + break; + } + } + } + + private static void SceneSaved(Scene scene) + { + EditorSceneManager.sceneSaved -= SceneSaved; + SceneDirtyState = SceneDirtyStates.Saved; + } + + } +#endif + /// /// A component used to identify that a GameObject in the network /// [AddComponentMenu("Netcode/Network Object", -99)] + [DisallowMultipleComponent] public sealed class NetworkObject : MonoBehaviour { [HideInInspector] [SerializeField] - internal uint GlobalObjectIdHash; + public uint GlobalObjectIdHash; /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 @@ -47,6 +129,13 @@ public uint PrefabIdHash } #if UNITY_EDITOR + + [ContextMenu("Update All In-Scene Placed Instances")] + private void UpdateAllPrefabInstances() + { + Debug.Log("TODO: Store the currently active scene, open all scenes within the scenes in build list, update all GlobalObjectIdHash values."); + } + private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; private void OnValidate() @@ -68,6 +157,11 @@ internal void GenerateGlobalObjectIdHash() return; } + if (gameObject.scene.name != null) + { + Debug.Log($"[{gameObject.name}] Scene: {gameObject.scene.name}"); + } + // Get a global object identifier for this network prefab var globalId = GetGlobalId(); @@ -83,25 +177,66 @@ internal void GenerateGlobalObjectIdHash() // If the GlobalObjectIdHash value changed, then mark the asset dirty if (GlobalObjectIdHash != oldValue) { - EditorUtility.SetDirty(this); + // Check if this is an in-scnee placed NetworkObject + if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name) + { + if (gameObject.name.Contains("TestGlobalObjectIdHash")) + { + Debug.Log($"[{gameObject.name}] Did not save its GlobalObjectIdHash value!"); + } + if (globalId.identifierType != 2) + { + Debug.LogWarning($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}!"); + } + + if (PrefabUtility.IsPartOfAnyPrefab(this)) + { + PrefabUtility.RecordPrefabInstancePropertyModifications(this); + } + + NetworkObjectManagement.SceneDirtyState = NetworkObjectManagement.SceneDirtyStates.Mark; + NetworkObjectManagement.TargetScene = gameObject.scene; + Debug.Log($"[{gameObject.name}][Pre-Save] GlobalObjectIdHash {GlobalObjectIdHash}!"); + } + else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it + { + EditorUtility.SetDirty(this); + } + } + else + { + if (gameObject.name.Contains("TestGlobalObjectIdHash")) + { + Debug.Log($"[{gameObject.name}] GlobalObjectIdHash {GlobalObjectIdHash}!"); + } } } - private GlobalObjectId GetGlobalId() - { - var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); + 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; + } + + private GlobalObjectId GetGlobalId() + { + var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); + + if (!IsEditingPrefab()) { return instanceGlobalId; } // If the asset doesn't exist at the given path, then return the object identifier - var theAsset = AssetDatabase.LoadAssetAtPath(stage.assetPath); + var theAsset = AssetDatabase.LoadAssetAtPath(PrefabStageUtility.GetPrefabStage(gameObject).assetPath); if (theAsset == null) { return instanceGlobalId; @@ -118,8 +253,14 @@ private GlobalObjectId GetGlobalId() // 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 (type 3). - var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, 3, guid, localFileId, 0); + // Object Types + // 0 = Null (we exit early on this type) + // 1 = Imported Asset + // 2 = Scene Object + // 3 = Source Asset. + var objetType = 3; + // Construct an imported asset identifier with the type being a source asset + var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, objetType, 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)) From 5d4b5df3c2c9d4bf0fbdb03d3ad0cf6c67a2661b Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Tue, 19 Sep 2023 18:10:06 -0500 Subject: [PATCH 02/10] style adding comment --- com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index f047b998c7..4992f1592d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -16,6 +16,10 @@ namespace Unity.Netcode { #if UNITY_EDITOR + /// + /// This would only need to be here if we have automatic updating of in-scene placed network prefab + /// instances upon loading a scene in the editor. + /// internal class NetworkObjectManagement { [InitializeOnLoadMethod] From bfc8d547052f906fca1cecdaf0a57dc4635593f2 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Tue, 19 Sep 2023 18:47:38 -0500 Subject: [PATCH 03/10] style Left out including using UnityEditor.SceneManagement for earlier builds --- com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 4992f1592d..ae579e759a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -6,6 +6,7 @@ #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else +using UnityEditor.SceneManagement; using UnityEditor.Experimental.SceneManagement; #endif #endif @@ -15,6 +16,7 @@ namespace Unity.Netcode { + #if UNITY_EDITOR /// /// This would only need to be here if we have automatic updating of in-scene placed network prefab From 8894da49f573312cc2d6e41acc50ece714a12972 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Wed, 20 Sep 2023 02:38:34 -0500 Subject: [PATCH 04/10] update Cleaned up some of the code. Put GlobalObjectIdHash back to internal. Added additional comments for draft POC review. --- .../Runtime/Core/NetworkObject.cs | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index ae579e759a..d04f8336b6 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -22,11 +22,19 @@ namespace Unity.Netcode /// This would only need to be here if we have automatic updating of in-scene placed network prefab /// instances upon loading a scene in the editor. /// + /// + /// This is not in editor assembly since NetworkObject needs access to this class. + /// TODO: Migrate class into its own file + /// internal class NetworkObjectManagement { + [InitializeOnLoadMethod] internal static void OnLoad() { + // Assure no double subscriptions + EditorApplication.update -= OnEditorUpdate; + // Subscribe to editor updates EditorApplication.update += OnEditorUpdate; } @@ -46,6 +54,8 @@ internal enum SceneDirtyStates internal static Scene TargetScene; + // TODO: If auto-save scenes with GlobalObjectIdHash updates is disabled then + // EditorApplication.update would not be subscribed to internal static void OnEditorUpdate() { if (SceneDirtyState == SceneDirtyStates.None) @@ -58,9 +68,12 @@ internal static void OnEditorUpdate() { if (EditorSceneManager.MarkSceneDirty(TargetScene)) { + // Just provide a small delay to allow the scene to become recognized as dirty s_MarkedDelay = Time.realtimeSinceStartup + 0.1f; - Debug.Log($"[{TargetScene.name}] marked as dirty!"); SceneDirtyState = SceneDirtyStates.Marked; + + // TODO: Remove logging before making full PR + Debug.Log($"[{TargetScene.name}] marked as dirty!"); } break; } @@ -68,8 +81,9 @@ internal static void OnEditorUpdate() { if (s_MarkedDelay < Time.realtimeSinceStartup) { - Debug.Log($"[{TargetScene.name}] Saving scene..."); SceneDirtyState = SceneDirtyStates.Save; + // TODO: Remove logging before making full PR + Debug.Log($"[{TargetScene.name}] Saving scene..."); } break; } @@ -80,12 +94,14 @@ internal static void OnEditorUpdate() SceneDirtyState = SceneDirtyStates.Saving; if (!EditorSceneManager.SaveScene(TargetScene)) { + // TODO: Show dialog to user regarding the failure to save the scene Debug.LogError($"[{TargetScene.name}] Failed to save scene!"); } break; } case SceneDirtyStates.Saved: { + // TODO: Remove logging before making full PR Debug.Log($"[{TargetScene.name}] Scene saved!"); SceneDirtyState = SceneDirtyStates.None; break; @@ -98,7 +114,6 @@ private static void SceneSaved(Scene scene) EditorSceneManager.sceneSaved -= SceneSaved; SceneDirtyState = SceneDirtyStates.Saved; } - } #endif @@ -112,7 +127,7 @@ public sealed class NetworkObject : MonoBehaviour { [HideInInspector] [SerializeField] - public uint GlobalObjectIdHash; + internal uint GlobalObjectIdHash; /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 @@ -163,14 +178,14 @@ internal void GenerateGlobalObjectIdHash() return; } - if (gameObject.scene.name != null) - { - Debug.Log($"[{gameObject.name}] Scene: {gameObject.scene.name}"); - } - // Get a global object identifier for this network prefab var globalId = GetGlobalId(); + // Object Types + // 0 = Null (when considered a null object type we can ignore) + // 1 = Imported Asset + // 2 = Scene Object + // 3 = Source Asset. // if the identifier type is 0, then don't update the GlobalObjectIdHash if (globalId.identifierType == 0) { @@ -183,26 +198,33 @@ internal void GenerateGlobalObjectIdHash() // If the GlobalObjectIdHash value changed, then mark the asset dirty if (GlobalObjectIdHash != oldValue) { - // Check if this is an in-scnee placed NetworkObject + // 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) { + // TODO: Remove before making full PR if (gameObject.name.Contains("TestGlobalObjectIdHash")) { Debug.Log($"[{gameObject.name}] Did not save its GlobalObjectIdHash value!"); } + + // Sanity check to make sure this is a scene placed object if (globalId.identifierType != 2) { - Debug.LogWarning($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}!"); + // 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 (PrefabUtility.IsPartOfAnyPrefab(this)) { + // We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty) PrefabUtility.RecordPrefabInstancePropertyModifications(this); } + // TODO: This will be dependent upon an NGO project setting and/or a context menu initiated action + // This is just a temporary way to validate the POC of the approach NetworkObjectManagement.SceneDirtyState = NetworkObjectManagement.SceneDirtyStates.Mark; NetworkObjectManagement.TargetScene = gameObject.scene; - Debug.Log($"[{gameObject.name}][Pre-Save] GlobalObjectIdHash {GlobalObjectIdHash}!"); } else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it { @@ -211,6 +233,7 @@ internal void GenerateGlobalObjectIdHash() } else { + // TODO: Remove before making full PR if (gameObject.name.Contains("TestGlobalObjectIdHash")) { Debug.Log($"[{gameObject.name}] GlobalObjectIdHash {GlobalObjectIdHash}!"); @@ -236,13 +259,22 @@ 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 theAsset = AssetDatabase.LoadAssetAtPath(PrefabStageUtility.GetPrefabStage(gameObject).assetPath); + 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; @@ -251,7 +283,6 @@ private GlobalObjectId GetGlobalId() // 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)) { - Debug.Log($"[GlobalObjectId Gen][{theAsset.gameObject.name}] Failed to get GUID or the local file identifier. Returning default ({instanceGlobalId})."); return instanceGlobalId; } @@ -271,11 +302,11 @@ private GlobalObjectId GetGlobalId() // 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})"); + Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})! ** Please Report This Error **"); return instanceGlobalId; } - // Otherwise, return the constructed identifier. + // Otherwise, return the constructed identifier for the source prefab asset return prefabGlobalId; } #endif // UNITY_EDITOR From 570f3ba7fa95e112535cd6abc7e67c0811f0a371 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Wed, 20 Sep 2023 02:40:38 -0500 Subject: [PATCH 05/10] style removing unintentional CR/LF --- com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index d04f8336b6..6bb44005c4 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -121,7 +121,6 @@ private static void SceneSaved(Scene scene) /// A component used to identify that a GameObject in the network /// [AddComponentMenu("Netcode/Network Object", -99)] - [DisallowMultipleComponent] public sealed class NetworkObject : MonoBehaviour { From 906fad3d6f7532c28c9b798e7be715db9ebc957d Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Wed, 20 Sep 2023 15:43:20 -0500 Subject: [PATCH 06/10] update Split apart the NetworkObjectRefreshTool from NetworkObject. Made some updates that don't require any form of editor application update. Added script in NetworkObject.RefreshAllPrefabInstances context menu method that handles refreshing the currently active scene and all enabled scenes in the build list. --- .../Runtime/Core/NetworkObject.cs | 153 ++++-------------- .../Runtime/Core/NetworkObjectRefreshTool.cs | 114 +++++++++++++ .../Core/NetworkObjectRefreshTool.cs.meta | 11 ++ .../MyNetworkObject.prefab | 54 +++++++ .../MyNetworkObject.prefab.meta | 7 + 5 files changed, 213 insertions(+), 126 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs.meta create mode 100644 testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab create mode 100644 testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 6bb44005c4..6472b68216 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -6,7 +6,6 @@ #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else -using UnityEditor.SceneManagement; using UnityEditor.Experimental.SceneManagement; #endif #endif @@ -16,107 +15,6 @@ namespace Unity.Netcode { - -#if UNITY_EDITOR - /// - /// This would only need to be here if we have automatic updating of in-scene placed network prefab - /// instances upon loading a scene in the editor. - /// - /// - /// This is not in editor assembly since NetworkObject needs access to this class. - /// TODO: Migrate class into its own file - /// - internal class NetworkObjectManagement - { - - [InitializeOnLoadMethod] - internal static void OnLoad() - { - // Assure no double subscriptions - EditorApplication.update -= OnEditorUpdate; - // Subscribe to editor updates - EditorApplication.update += OnEditorUpdate; - } - - internal enum SceneDirtyStates - { - None, - Mark, - Marked, - Save, - Saving, - Saved - } - - internal static SceneDirtyStates SceneDirtyState; - - private static float s_MarkedDelay; - - internal static Scene TargetScene; - - // TODO: If auto-save scenes with GlobalObjectIdHash updates is disabled then - // EditorApplication.update would not be subscribed to - internal static void OnEditorUpdate() - { - if (SceneDirtyState == SceneDirtyStates.None) - { - return; - } - switch (SceneDirtyState) - { - case SceneDirtyStates.Mark: - { - if (EditorSceneManager.MarkSceneDirty(TargetScene)) - { - // Just provide a small delay to allow the scene to become recognized as dirty - s_MarkedDelay = Time.realtimeSinceStartup + 0.1f; - SceneDirtyState = SceneDirtyStates.Marked; - - // TODO: Remove logging before making full PR - Debug.Log($"[{TargetScene.name}] marked as dirty!"); - } - break; - } - case SceneDirtyStates.Marked: - { - if (s_MarkedDelay < Time.realtimeSinceStartup) - { - SceneDirtyState = SceneDirtyStates.Save; - // TODO: Remove logging before making full PR - Debug.Log($"[{TargetScene.name}] Saving scene..."); - } - break; - } - case SceneDirtyStates.Save: - { - s_MarkedDelay = 0.0f; - EditorSceneManager.sceneSaved += SceneSaved; - SceneDirtyState = SceneDirtyStates.Saving; - if (!EditorSceneManager.SaveScene(TargetScene)) - { - // TODO: Show dialog to user regarding the failure to save the scene - Debug.LogError($"[{TargetScene.name}] Failed to save scene!"); - } - break; - } - case SceneDirtyStates.Saved: - { - // TODO: Remove logging before making full PR - Debug.Log($"[{TargetScene.name}] Scene saved!"); - SceneDirtyState = SceneDirtyStates.None; - break; - } - } - } - - private static void SceneSaved(Scene scene) - { - EditorSceneManager.sceneSaved -= SceneSaved; - SceneDirtyState = SceneDirtyStates.Saved; - } - } -#endif - /// /// A component used to identify that a GameObject in the network /// @@ -149,14 +47,35 @@ public uint PrefabIdHash } #if UNITY_EDITOR + private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; - [ContextMenu("Update All In-Scene Placed Instances")] - private void UpdateAllPrefabInstances() + [ContextMenu("Refresh In-Scene Prefab Instances")] + private void RefreshAllPrefabInstances() { - Debug.Log("TODO: Store the currently active scene, open all scenes within the scenes in build list, update all GlobalObjectIdHash values."); - } + // Handle updating the currently active scene + var networkObjects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + foreach (var networkObject in networkObjects) + { + networkObject.OnValidate(); + } + NetworkObjectRefreshTool.ProcessActiveScene(); - private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; + // Refresh all build settings scenes + var activeScene = SceneManager.GetActiveScene(); + foreach (var editorScene in EditorBuildSettings.scenes) + { + // skip disabled scenes and the currently active scene + if (!editorScene.enabled || activeScene.path == editorScene.path) + { + continue; + } + // Add the scene to be processed + NetworkObjectRefreshTool.ProcessScene(editorScene.path, false); + } + + // Process all added scenes + NetworkObjectRefreshTool.ProcessScenes(); + } private void OnValidate() { @@ -200,12 +119,6 @@ internal void GenerateGlobalObjectIdHash() // 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) { - // TODO: Remove before making full PR - if (gameObject.name.Contains("TestGlobalObjectIdHash")) - { - Debug.Log($"[{gameObject.name}] Did not save its GlobalObjectIdHash value!"); - } - // Sanity check to make sure this is a scene placed object if (globalId.identifierType != 2) { @@ -220,27 +133,15 @@ internal void GenerateGlobalObjectIdHash() PrefabUtility.RecordPrefabInstancePropertyModifications(this); } - // TODO: This will be dependent upon an NGO project setting and/or a context menu initiated action - // This is just a temporary way to validate the POC of the approach - NetworkObjectManagement.SceneDirtyState = NetworkObjectManagement.SceneDirtyStates.Mark; - NetworkObjectManagement.TargetScene = gameObject.scene; + NetworkObjectRefreshTool.ProcessScene(gameObject.scene.path); } else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it { EditorUtility.SetDirty(this); } } - else - { - // TODO: Remove before making full PR - if (gameObject.name.Contains("TestGlobalObjectIdHash")) - { - Debug.Log($"[{gameObject.name}] GlobalObjectIdHash {GlobalObjectIdHash}!"); - } - } } - private bool IsEditingPrefab() { // Check if we are directly editing the prefab diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs new file mode 100644 index 0000000000..7040652d8d --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs @@ -0,0 +1,114 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.Linq; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Unity.Netcode +{ + /// + /// This is a helper tool to update all in-scene placed instances of a prefab that + /// originally did not have a NetworkObject component but one was added to the prefab + /// later. + /// + internal class NetworkObjectRefreshTool + { + private static List s_ScenesToUpdate = new List(); + private static bool s_ProcessScenes; + private static bool s_CloseScenes; + + internal static void ProcessScene(string scenePath, bool processScenes = true) + { + if (!s_ScenesToUpdate.Contains(scenePath)) + { + if (s_ScenesToUpdate.Count == 0) + { + EditorSceneManager.sceneOpened += EditorSceneManager_sceneOpened; + EditorSceneManager.sceneSaved += EditorSceneManager_sceneSaved; + } + s_ScenesToUpdate.Add(scenePath); + } + s_ProcessScenes = processScenes; + } + + internal static void ProcessActiveScene() + { + var activeScene = SceneManager.GetActiveScene(); + if (s_ScenesToUpdate.Contains(activeScene.path) && s_ProcessScenes) + { + SceneOpened(activeScene); + } + } + + internal static void ProcessScenes() + { + if (s_ScenesToUpdate.Count != 0) + { + s_CloseScenes = true; + var scenePath = s_ScenesToUpdate.First(); + EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); + } + else + { + s_CloseScenes = false; + EditorSceneManager.sceneSaved -= EditorSceneManager_sceneSaved; + EditorSceneManager.sceneOpened -= EditorSceneManager_sceneOpened; + } + } + + private static void FinishedProcessingScene(Scene scene, bool refreshed = false) + { + if (s_ScenesToUpdate.Contains(scene.path)) + { + // Provide a log of all scenes that were modified to the user + if (refreshed) + { + Debug.Log($"Refreshed and saved updates to scene: {scene.name}"); + } + s_ProcessScenes = false; + s_ScenesToUpdate.Remove(scene.path); + + if (scene != SceneManager.GetActiveScene()) + { + EditorSceneManager.CloseScene(scene, s_CloseScenes); + } + ProcessScenes(); + } + } + + private static void EditorSceneManager_sceneSaved(Scene scene) + { + FinishedProcessingScene(scene, true); + } + + private static void SceneOpened(Scene scene) + { + if (s_ScenesToUpdate.Contains(scene.path)) + { + if (s_ProcessScenes) + { + if (!EditorSceneManager.MarkSceneDirty(scene)) + { + Debug.Log($"Scene {scene.name} did not get marked as dirty!"); + FinishedProcessingScene(scene); + } + else + { + EditorSceneManager.SaveScene(scene); + } + } + else + { + FinishedProcessingScene(scene); + } + } + } + + private static void EditorSceneManager_sceneOpened(Scene scene, OpenSceneMode mode) + { + SceneOpened(scene); + } + } +} +#endif // UNITY_EDITOR diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs.meta b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs.meta new file mode 100644 index 0000000000..58a3178608 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d24d5e8371c3cca4890e2713bdeda288 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab new file mode 100644 index 0000000000..8d363b88f1 --- /dev/null +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab @@ -0,0 +1,54 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1445163809825623502 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4895178628090191772} + - component: {fileID: 7314688500250972154} + m_Layer: 0 + m_Name: MyNetworkObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4895178628090191772 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1445163809825623502} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &7314688500250972154 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1445163809825623502} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 0 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta new file mode 100644 index 0000000000..b317937762 --- /dev/null +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1cd10f8f7aebaf84b8f8b30edf0cf43a +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 76837ca3e7f2567113179d8d158d839e56a4b293 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Wed, 20 Sep 2023 19:23:17 -0500 Subject: [PATCH 07/10] update Removing accidental addition of a test prefab. Adding dialog notification when attempting to do a NetworkObject Refresh on an in-scene placed prefab instance as opposed to a prefab instance. --- .../Runtime/Core/NetworkObject.cs | 7 +++ .../MyNetworkObject.prefab | 54 ------------------- .../MyNetworkObject.prefab.meta | 7 --- 3 files changed, 7 insertions(+), 61 deletions(-) delete mode 100644 testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab delete mode 100644 testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 6472b68216..3ba1bd1f08 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -52,6 +52,13 @@ public uint PrefabIdHash [ContextMenu("Refresh In-Scene Prefab Instances")] private void RefreshAllPrefabInstances() { + var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); + if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != 1) + { + EditorUtility.DisplayDialog("Network Prefab Assets Only", "This action can only be performed on a network prefab asset.", "Ok"); + return; + } + // Handle updating the currently active scene var networkObjects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var networkObject in networkObjects) diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab deleted file mode 100644 index 8d363b88f1..0000000000 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab +++ /dev/null @@ -1,54 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1 &1445163809825623502 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 4895178628090191772} - - component: {fileID: 7314688500250972154} - m_Layer: 0 - m_Name: MyNetworkObject - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &4895178628090191772 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1445163809825623502} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &7314688500250972154 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1445163809825623502} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} - m_Name: - m_EditorClassIdentifier: - GlobalObjectIdHash: 0 - AlwaysReplicateAsRoot: 0 - SynchronizeTransform: 1 - ActiveSceneSynchronization: 0 - SceneMigrationSynchronization: 1 - SpawnWithObservers: 1 - DontDestroyWithOwner: 0 - AutoObjectParentSync: 1 diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta deleted file mode 100644 index b317937762..0000000000 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/MyNetworkObject.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 1cd10f8f7aebaf84b8f8b30edf0cf43a -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: From 934cba45a84793d3b87a1f7d84da468eefe9e69e Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 21 Sep 2023 17:30:00 -0500 Subject: [PATCH 08/10] test updates for the manual test --- .../Runtime/Core/NetworkObject.cs | 2 +- .../Runtime/Core/NetworkObjectRefreshTool.cs | 4 ++++ .../Runtime/NetcodeIntegrationTestHelpers.cs | 13 +++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 3ba1bd1f08..f6a713d14c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -50,7 +50,7 @@ public uint PrefabIdHash private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; [ContextMenu("Refresh In-Scene Prefab Instances")] - private void RefreshAllPrefabInstances() + internal void RefreshAllPrefabInstances() { var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != 1) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs index 7040652d8d..b9c6db0cf9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs @@ -1,4 +1,5 @@ #if UNITY_EDITOR +using System; using System.Collections.Generic; using System.Linq; using UnityEditor.SceneManagement; @@ -18,6 +19,8 @@ internal class NetworkObjectRefreshTool private static bool s_ProcessScenes; private static bool s_CloseScenes; + internal static Action AllScenesProcessed; + internal static void ProcessScene(string scenePath, bool processScenes = true) { if (!s_ScenesToUpdate.Contains(scenePath)) @@ -54,6 +57,7 @@ internal static void ProcessScenes() s_CloseScenes = false; EditorSceneManager.sceneSaved -= EditorSceneManager_sceneSaved; EditorSceneManager.sceneOpened -= EditorSceneManager_sceneOpened; + AllScenesProcessed?.Invoke(); } } diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index 90f19624fd..8e6cf7b2be 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -924,6 +924,19 @@ public static uint GetGlobalObjectIdHash(NetworkObject networkObject) { return networkObject.GlobalObjectIdHash; } + +#if UNITY_EDITOR + public static void SetRefreshAllPrefabsCallback(Action scenesProcessed) + { + NetworkObjectRefreshTool.AllScenesProcessed = scenesProcessed; + } + + public static void RefreshAllPrefabInstances(NetworkObject networkObject, Action scenesProcessed) + { + NetworkObjectRefreshTool.AllScenesProcessed = scenesProcessed; + networkObject.RefreshAllPrefabInstances(); + } +#endif } // Empty MonoBehaviour that is a holder of coroutine From b25fbbe96b2628c16c5bdaaaacd5ef25e59a4cf0 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Thu, 21 Sep 2023 17:35:32 -0500 Subject: [PATCH 09/10] update adding change log entry --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 1b8180ae57..d86ea3e9da 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- Added context menu tool that provides users with the ability to quickly update the GlobalObjectIdHash value for all in-scene placed prefab instances that were created prior to adding a NetworkObject component to it. (#2707) - Added methods NetworkManager.SetPeerMTU and NetworkManager.GetPeerMTU to be able to set MTU sizes per-peer (#2676) ### Fixed From 29aa6ee6e26d6f77e6d5f7af02d1a5028ee78f8a Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 25 Sep 2023 10:15:42 -0500 Subject: [PATCH 10/10] style Added object type identifier constants for code clarity purposes. --- .../Runtime/Core/NetworkObject.cs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index f6a713d14c..2055d766a6 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -49,11 +49,23 @@ public uint PrefabIdHash #if UNITY_EDITOR private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; + /// + /// Object Types + /// + // 0 = Null (when considered a null object type we can ignore) + // 1 = Imported Asset + // 2 = Scene Object + // 3 = Source Asset. + private const int k_NullObjectType = 0; + private const int k_ImportedAssetObjectType = 1; + private const int k_SceneObjectType = 2; + private const int k_SourceAssetObjectType = 3; + [ContextMenu("Refresh In-Scene Prefab Instances")] internal void RefreshAllPrefabInstances() { var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); - if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != 1) + if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != k_ImportedAssetObjectType) { EditorUtility.DisplayDialog("Network Prefab Assets Only", "This action can only be performed on a network prefab asset.", "Ok"); return; @@ -106,13 +118,9 @@ internal void GenerateGlobalObjectIdHash() // Get a global object identifier for this network prefab var globalId = GetGlobalId(); - // Object Types - // 0 = Null (when considered a null object type we can ignore) - // 1 = Imported Asset - // 2 = Scene Object - // 3 = Source Asset. + // if the identifier type is 0, then don't update the GlobalObjectIdHash - if (globalId.identifierType == 0) + if (globalId.identifierType == k_NullObjectType) { return; } @@ -127,7 +135,7 @@ internal void GenerateGlobalObjectIdHash() if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name) { // Sanity check to make sure this is a scene placed object - if (globalId.identifierType != 2) + if (globalId.identifierType != k_SceneObjectType) { // 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**"); @@ -193,18 +201,12 @@ private GlobalObjectId GetGlobalId() return instanceGlobalId; } - // If we reached this point, then we are most likely opening a prefab to edit. + // 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). - // Object Types - // 0 = Null (we exit early on this type) - // 1 = Imported Asset - // 2 = Scene Object - // 3 = Source Asset. - var objetType = 3; - // Construct an imported asset identifier with the type being a source asset - var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, objetType, guid, (ulong)localFileId, 0); + // 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))