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