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 diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 64bf640627..2055d766a6 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -49,6 +49,53 @@ 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 != k_ImportedAssetObjectType) + { + 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) + { + networkObject.OnValidate(); + } + NetworkObjectRefreshTool.ProcessActiveScene(); + + // 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() { GenerateGlobalObjectIdHash(); @@ -71,8 +118,9 @@ internal void GenerateGlobalObjectIdHash() // Get a global object identifier for this network prefab var globalId = GetGlobalId(); + // if the identifier type is 0, then don't update the GlobalObjectIdHash - if (globalId.identifierType == 0) + if (globalId.identifierType == k_NullObjectType) { return; } @@ -83,25 +131,65 @@ 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 (Special Case for In-Scene Placed) + 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 != 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**"); + } + + // 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); + } + + 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); + } } } - private GlobalObjectId GetGlobalId() + private bool IsEditingPrefab() { - var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); - // 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 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(stage.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; @@ -110,25 +198,24 @@ 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; } - // 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). - // Construct an imported asset identifier with the type being a source asset (type 3). - var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, 3, guid, 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)) { - 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 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..b9c6db0cf9 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs @@ -0,0 +1,118 @@ +#if UNITY_EDITOR +using System; +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 Action AllScenesProcessed; + + 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; + AllScenesProcessed?.Invoke(); + } + } + + 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/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