Skip to content

Commit c84a1dc

Browse files
fix: GlobalObjectIdHash generation for already existing in-scene placed prefab instances [MTT-7055] (#2707)
* update Resolves MTT-7055. 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. Added dialog notification when attempting to do a NetworkObject Refresh on an in-scene placed prefab instance as opposed to a prefab instance. * test Made some minor updates for the manual test * update adding change log entry * style Added object type identifier constants for code clarity purposes.
1 parent ca577a6 commit c84a1dc

File tree

5 files changed

+242
-12
lines changed

5 files changed

+242
-12
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1010

1111
### Added
1212

13+
- 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)
1314
- Added methods NetworkManager.SetPeerMTU and NetworkManager.GetPeerMTU to be able to set MTU sizes per-peer (#2676)
1415

1516
### Fixed

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,53 @@ public uint PrefabIdHash
4949
#if UNITY_EDITOR
5050
private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}";
5151

52+
/// <summary>
53+
/// Object Types <see href="https://docs.unity3d.com/ScriptReference/GlobalObjectId.html"/>
54+
/// </summary>
55+
// 0 = Null (when considered a null object type we can ignore)
56+
// 1 = Imported Asset
57+
// 2 = Scene Object
58+
// 3 = Source Asset.
59+
private const int k_NullObjectType = 0;
60+
private const int k_ImportedAssetObjectType = 1;
61+
private const int k_SceneObjectType = 2;
62+
private const int k_SourceAssetObjectType = 3;
63+
64+
[ContextMenu("Refresh In-Scene Prefab Instances")]
65+
internal void RefreshAllPrefabInstances()
66+
{
67+
var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
68+
if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != k_ImportedAssetObjectType)
69+
{
70+
EditorUtility.DisplayDialog("Network Prefab Assets Only", "This action can only be performed on a network prefab asset.", "Ok");
71+
return;
72+
}
73+
74+
// Handle updating the currently active scene
75+
var networkObjects = FindObjectsByType<NetworkObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
76+
foreach (var networkObject in networkObjects)
77+
{
78+
networkObject.OnValidate();
79+
}
80+
NetworkObjectRefreshTool.ProcessActiveScene();
81+
82+
// Refresh all build settings scenes
83+
var activeScene = SceneManager.GetActiveScene();
84+
foreach (var editorScene in EditorBuildSettings.scenes)
85+
{
86+
// skip disabled scenes and the currently active scene
87+
if (!editorScene.enabled || activeScene.path == editorScene.path)
88+
{
89+
continue;
90+
}
91+
// Add the scene to be processed
92+
NetworkObjectRefreshTool.ProcessScene(editorScene.path, false);
93+
}
94+
95+
// Process all added scenes
96+
NetworkObjectRefreshTool.ProcessScenes();
97+
}
98+
5299
private void OnValidate()
53100
{
54101
GenerateGlobalObjectIdHash();
@@ -71,8 +118,9 @@ internal void GenerateGlobalObjectIdHash()
71118
// Get a global object identifier for this network prefab
72119
var globalId = GetGlobalId();
73120

121+
74122
// if the identifier type is 0, then don't update the GlobalObjectIdHash
75-
if (globalId.identifierType == 0)
123+
if (globalId.identifierType == k_NullObjectType)
76124
{
77125
return;
78126
}
@@ -83,25 +131,65 @@ internal void GenerateGlobalObjectIdHash()
83131
// If the GlobalObjectIdHash value changed, then mark the asset dirty
84132
if (GlobalObjectIdHash != oldValue)
85133
{
86-
EditorUtility.SetDirty(this);
134+
// Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed)
135+
if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name)
136+
{
137+
// Sanity check to make sure this is a scene placed object
138+
if (globalId.identifierType != k_SceneObjectType)
139+
{
140+
// This should never happen, but in the event it does throw and error
141+
Debug.LogError($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}! **Report this error**");
142+
}
143+
144+
// If this is a prefab instance
145+
if (PrefabUtility.IsPartOfAnyPrefab(this))
146+
{
147+
// We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty)
148+
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
149+
}
150+
151+
NetworkObjectRefreshTool.ProcessScene(gameObject.scene.path);
152+
}
153+
else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it
154+
{
155+
EditorUtility.SetDirty(this);
156+
}
87157
}
88158
}
89159

90-
private GlobalObjectId GetGlobalId()
160+
private bool IsEditingPrefab()
91161
{
92-
var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
93-
94162
// Check if we are directly editing the prefab
95163
var stage = PrefabStageUtility.GetPrefabStage(gameObject);
96164

97165
// if we are not editing the prefab directly (or a sub-prefab), then return the object identifier
98166
if (stage == null || stage.assetPath == null)
167+
{
168+
return false;
169+
}
170+
return true;
171+
}
172+
173+
private GlobalObjectId GetGlobalId()
174+
{
175+
var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
176+
177+
// If not editing a prefab, then just use the generated id
178+
if (!IsEditingPrefab())
99179
{
100180
return instanceGlobalId;
101181
}
102182

103183
// If the asset doesn't exist at the given path, then return the object identifier
104-
var theAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(stage.assetPath);
184+
var prefabStageAssetPath = PrefabStageUtility.GetPrefabStage(gameObject).assetPath;
185+
// If (for some reason) the asset path is null return the generated id
186+
if (prefabStageAssetPath == null)
187+
{
188+
return instanceGlobalId;
189+
}
190+
191+
var theAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(prefabStageAssetPath);
192+
// If there is no asset at that path (for some odd/edge case reason), return the generated id
105193
if (theAsset == null)
106194
{
107195
return instanceGlobalId;
@@ -110,25 +198,24 @@ private GlobalObjectId GetGlobalId()
110198
// If we can't get the asset GUID and/or the file identifier, then return the object identifier
111199
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(theAsset, out var guid, out long localFileId))
112200
{
113-
Debug.Log($"[GlobalObjectId Gen][{theAsset.gameObject.name}] Failed to get GUID or the local file identifier. Returning default ({instanceGlobalId}).");
114201
return instanceGlobalId;
115202
}
116203

117-
// If we reached this point, then we are most likely opening a prefab to edit.
204+
// Note: If we reached this point, then we are most likely opening a prefab to edit.
118205
// The instanceGlobalId will be constructed as if it is a scene object, however when it
119206
// is serialized its value will be treated as a file asset (the "why" to the below code).
120207

121-
// Construct an imported asset identifier with the type being a source asset (type 3).
122-
var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, 3, guid, localFileId, 0);
208+
// Construct an imported asset identifier with the type being a source asset object type
209+
var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, k_SourceAssetObjectType, guid, (ulong)localFileId, 0);
123210

124211
// If we can't parse the result log an error and return the instanceGlobalId
125212
if (!GlobalObjectId.TryParse(prefabGlobalIdText, out var prefabGlobalId))
126213
{
127-
Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})");
214+
Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})! ** Please Report This Error **");
128215
return instanceGlobalId;
129216
}
130217

131-
// Otherwise, return the constructed identifier.
218+
// Otherwise, return the constructed identifier for the source prefab asset
132219
return prefabGlobalId;
133220
}
134221
#endif // UNITY_EDITOR
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#if UNITY_EDITOR
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using UnityEditor.SceneManagement;
6+
using UnityEngine;
7+
using UnityEngine.SceneManagement;
8+
9+
namespace Unity.Netcode
10+
{
11+
/// <summary>
12+
/// This is a helper tool to update all in-scene placed instances of a prefab that
13+
/// originally did not have a NetworkObject component but one was added to the prefab
14+
/// later.
15+
/// </summary>
16+
internal class NetworkObjectRefreshTool
17+
{
18+
private static List<string> s_ScenesToUpdate = new List<string>();
19+
private static bool s_ProcessScenes;
20+
private static bool s_CloseScenes;
21+
22+
internal static Action AllScenesProcessed;
23+
24+
internal static void ProcessScene(string scenePath, bool processScenes = true)
25+
{
26+
if (!s_ScenesToUpdate.Contains(scenePath))
27+
{
28+
if (s_ScenesToUpdate.Count == 0)
29+
{
30+
EditorSceneManager.sceneOpened += EditorSceneManager_sceneOpened;
31+
EditorSceneManager.sceneSaved += EditorSceneManager_sceneSaved;
32+
}
33+
s_ScenesToUpdate.Add(scenePath);
34+
}
35+
s_ProcessScenes = processScenes;
36+
}
37+
38+
internal static void ProcessActiveScene()
39+
{
40+
var activeScene = SceneManager.GetActiveScene();
41+
if (s_ScenesToUpdate.Contains(activeScene.path) && s_ProcessScenes)
42+
{
43+
SceneOpened(activeScene);
44+
}
45+
}
46+
47+
internal static void ProcessScenes()
48+
{
49+
if (s_ScenesToUpdate.Count != 0)
50+
{
51+
s_CloseScenes = true;
52+
var scenePath = s_ScenesToUpdate.First();
53+
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
54+
}
55+
else
56+
{
57+
s_CloseScenes = false;
58+
EditorSceneManager.sceneSaved -= EditorSceneManager_sceneSaved;
59+
EditorSceneManager.sceneOpened -= EditorSceneManager_sceneOpened;
60+
AllScenesProcessed?.Invoke();
61+
}
62+
}
63+
64+
private static void FinishedProcessingScene(Scene scene, bool refreshed = false)
65+
{
66+
if (s_ScenesToUpdate.Contains(scene.path))
67+
{
68+
// Provide a log of all scenes that were modified to the user
69+
if (refreshed)
70+
{
71+
Debug.Log($"Refreshed and saved updates to scene: {scene.name}");
72+
}
73+
s_ProcessScenes = false;
74+
s_ScenesToUpdate.Remove(scene.path);
75+
76+
if (scene != SceneManager.GetActiveScene())
77+
{
78+
EditorSceneManager.CloseScene(scene, s_CloseScenes);
79+
}
80+
ProcessScenes();
81+
}
82+
}
83+
84+
private static void EditorSceneManager_sceneSaved(Scene scene)
85+
{
86+
FinishedProcessingScene(scene, true);
87+
}
88+
89+
private static void SceneOpened(Scene scene)
90+
{
91+
if (s_ScenesToUpdate.Contains(scene.path))
92+
{
93+
if (s_ProcessScenes)
94+
{
95+
if (!EditorSceneManager.MarkSceneDirty(scene))
96+
{
97+
Debug.Log($"Scene {scene.name} did not get marked as dirty!");
98+
FinishedProcessingScene(scene);
99+
}
100+
else
101+
{
102+
EditorSceneManager.SaveScene(scene);
103+
}
104+
}
105+
else
106+
{
107+
FinishedProcessingScene(scene);
108+
}
109+
}
110+
}
111+
112+
private static void EditorSceneManager_sceneOpened(Scene scene, OpenSceneMode mode)
113+
{
114+
SceneOpened(scene);
115+
}
116+
}
117+
}
118+
#endif // UNITY_EDITOR

com.unity.netcode.gameobjects/Runtime/Core/NetworkObjectRefreshTool.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,19 @@ public static uint GetGlobalObjectIdHash(NetworkObject networkObject)
924924
{
925925
return networkObject.GlobalObjectIdHash;
926926
}
927+
928+
#if UNITY_EDITOR
929+
public static void SetRefreshAllPrefabsCallback(Action scenesProcessed)
930+
{
931+
NetworkObjectRefreshTool.AllScenesProcessed = scenesProcessed;
932+
}
933+
934+
public static void RefreshAllPrefabInstances(NetworkObject networkObject, Action scenesProcessed)
935+
{
936+
NetworkObjectRefreshTool.AllScenesProcessed = scenesProcessed;
937+
networkObject.RefreshAllPrefabInstances();
938+
}
939+
#endif
927940
}
928941

929942
// Empty MonoBehaviour that is a holder of coroutine

0 commit comments

Comments
 (0)