Skip to content

Commit 5e38c31

Browse files
fix: prefab incontext or inIsolation edit mode globalobjectidhash incorrect when using MPPM (#3162)
* fix This fixes the issue with the GlobalObjectId value being incorrect when entering into play mode while still in prefab edit mode (InContext or InIsolation). * fix Minor fix to how we shutdown NetworkManager when exiting playmode to assure we follow the same steps as we do when the application quits. * fix InSceneObject needed to be checked for having a value before checking for the value. * update Removed legacy methods used to detect when in edit mode. Updated comments. * update Adding changelog entry.
1 parent 88eaa08 commit 5e38c31

File tree

3 files changed

+124
-83
lines changed

3 files changed

+124
-83
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

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

1616
### Fixed
1717

18+
- 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)
1819
- Fixed issue where a sever only `NetworkManager` instance would spawn the actual `NetworkPrefab`'s `GameObject` as opposed to creating an instance of it. (#3160)
1920
- 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)
2021
- Fixed issue where an exception was thrown when calling `NetworkManager.Shutdown` after calling `UnityTransport.Shutdown`. (#3118)

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,8 +1023,7 @@ private void ModeChanged(PlayModeStateChange change)
10231023
{
10241024
if (IsListening && change == PlayModeStateChange.ExitingPlayMode)
10251025
{
1026-
// Make sure we are not holding onto anything in case domain reload is disabled
1027-
ShutdownInternal();
1026+
OnApplicationQuit();
10281027
}
10291028
}
10301029
#endif

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

Lines changed: 122 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ public uint PrefabIdHash
102102
private const int k_SceneObjectType = 2;
103103
private const int k_SourceAssetObjectType = 3;
104104

105+
// Used to track any InContext or InIsolation prefab being edited.
106+
private static PrefabStage s_PrefabStage;
107+
// The network prefab asset that the edit mode scene has created an instance of (s_PrefabInstance).
108+
private static NetworkObject s_PrefabAsset;
109+
// The InContext or InIsolation edit mode network prefab scene instance of the prefab asset (s_PrefabAsset).
110+
private static NetworkObject s_PrefabInstance;
111+
112+
private static bool s_DebugPrefabIdGeneration;
113+
114+
105115
[ContextMenu("Refresh In-Scene Prefab Instances")]
106116
internal void RefreshAllPrefabInstances()
107117
{
@@ -134,25 +144,119 @@ internal void RefreshAllPrefabInstances()
134144
NetworkObjectRefreshTool.ProcessScenes();
135145
}
136146

147+
/// <summary>
148+
/// Register for <see cref="PrefabStage"/> opened and closing event notifications.
149+
/// </summary>
150+
[InitializeOnLoadMethod]
151+
private static void OnApplicationStart()
152+
{
153+
PrefabStage.prefabStageOpened -= PrefabStageOpened;
154+
PrefabStage.prefabStageOpened += PrefabStageOpened;
155+
PrefabStage.prefabStageClosing -= PrefabStageClosing;
156+
PrefabStage.prefabStageClosing += PrefabStageClosing;
157+
}
158+
159+
private static void PrefabStageClosing(PrefabStage prefabStage)
160+
{
161+
// If domain reloading is enabled, then this will be null when we return from playmode.
162+
if (s_PrefabStage == null)
163+
{
164+
// Determine if we have a network prefab opened in edit mode or not.
165+
CheckPrefabStage(prefabStage);
166+
}
167+
168+
s_PrefabStage = null;
169+
s_PrefabInstance = null;
170+
s_PrefabAsset = null;
171+
}
172+
173+
private static void PrefabStageOpened(PrefabStage prefabStage)
174+
{
175+
// Determine if we have a network prefab opened in edit mode or not.
176+
CheckPrefabStage(prefabStage);
177+
}
178+
179+
/// <summary>
180+
/// Determines if we have opened a network prefab in edit mode (InContext or InIsolation)
181+
/// </summary>
182+
/// <remarks>
183+
/// InContext: Typically means a are in prefab edit mode for an in-scene placed network prefab instance.
184+
/// (currently no such thing as a network prefab with nested network prefab instances)
185+
///
186+
/// InIsolation: Typically means we are in prefb edit mode for a prefab asset.
187+
/// </remarks>
188+
/// <param name="prefabStage"></param>
189+
private static void CheckPrefabStage(PrefabStage prefabStage)
190+
{
191+
s_PrefabStage = prefabStage;
192+
s_PrefabInstance = prefabStage.prefabContentsRoot?.GetComponent<NetworkObject>();
193+
if (s_PrefabInstance)
194+
{
195+
// We acquire the source prefab that the prefab edit mode scene instance was instantiated from differently for InContext than InSolation.
196+
if (s_PrefabStage.mode == PrefabStage.Mode.InContext && s_PrefabStage.openedFromInstanceRoot != null)
197+
{
198+
// This is needed to handle the scenario where a user completely loads a new scene while in an InContext prefab edit mode.
199+
try
200+
{
201+
s_PrefabAsset = s_PrefabStage.openedFromInstanceRoot?.GetComponent<NetworkObject>();
202+
}
203+
catch
204+
{
205+
s_PrefabAsset = null;
206+
}
207+
}
208+
else
209+
{
210+
// When editing in InIsolation mode, load the original prefab asset from the provided path.
211+
s_PrefabAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(s_PrefabStage.assetPath);
212+
}
213+
214+
if (s_PrefabInstance.GlobalObjectIdHash != s_PrefabAsset.GlobalObjectIdHash)
215+
{
216+
s_PrefabInstance.GlobalObjectIdHash = s_PrefabAsset.GlobalObjectIdHash;
217+
// For InContext mode, we don't want to record these modifications (the in-scene GlobalObjectIdHash is serialized with the scene).
218+
if (s_PrefabStage.mode == PrefabStage.Mode.InIsolation)
219+
{
220+
PrefabUtility.RecordPrefabInstancePropertyModifications(s_PrefabAsset);
221+
}
222+
}
223+
}
224+
else
225+
{
226+
s_PrefabStage = null;
227+
s_PrefabInstance = null;
228+
s_PrefabAsset = null;
229+
}
230+
}
231+
232+
/// <summary>
233+
/// GlobalObjectIdHash values are generated during validation.
234+
/// </summary>
137235
internal void OnValidate()
138236
{
139-
// do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode
237+
// Always exit early if we are in prefab edit mode and this instance is the
238+
// prefab instance within the InContext or InIsolation edit scene.
239+
if (s_PrefabInstance == this)
240+
{
241+
return;
242+
}
243+
244+
// Do not regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in play mode.
140245
if (EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
141246
{
142247
return;
143248
}
144249

145-
// do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode
250+
// Do not regenerate GlobalObjectIdHash if Editor is transitioning into or out of play mode.
146251
if (!EditorApplication.isPlaying && EditorApplication.isPlayingOrWillChangePlaymode)
147252
{
148253
return;
149254
}
150255

151-
// Get a global object identifier for this network prefab
152-
var globalId = GetGlobalId();
153-
256+
// Get a global object identifier for this network prefab.
257+
var globalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
154258

155-
// if the identifier type is 0, then don't update the GlobalObjectIdHash
259+
// if the identifier type is 0, then don't update the GlobalObjectIdHash.
156260
if (globalId.identifierType == k_NullObjectType)
157261
{
158262
return;
@@ -161,47 +265,34 @@ internal void OnValidate()
161265
var oldValue = GlobalObjectIdHash;
162266
GlobalObjectIdHash = globalId.ToString().Hash32();
163267

164-
// If the GlobalObjectIdHash value changed, then mark the asset dirty
268+
// Always check for in-scene placed to assure any previous version scene assets with in-scene place NetworkObjects gets updated.
269+
CheckForInScenePlaced();
270+
271+
// If the GlobalObjectIdHash value changed, then mark the asset dirty.
165272
if (GlobalObjectIdHash != oldValue)
166273
{
167-
// Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed)
168-
if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name)
274+
// Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed).
275+
if (IsSceneObject.HasValue && IsSceneObject.Value)
169276
{
170-
// Sanity check to make sure this is a scene placed object
277+
// Sanity check to make sure this is a scene placed object.
171278
if (globalId.identifierType != k_SceneObjectType)
172279
{
173-
// This should never happen, but in the event it does throw and error
280+
// This should never happen, but in the event it does throw and error.
174281
Debug.LogError($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}! **Report this error**");
175282
}
176283

177-
// If this is a prefab instance
284+
// 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.
178285
if (PrefabUtility.IsPartOfAnyPrefab(this))
179286
{
180-
// We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty)
287+
// We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty).
181288
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
182289
}
183290
}
184-
else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it
291+
else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it.
185292
{
186293
EditorUtility.SetDirty(this);
187294
}
188295
}
189-
190-
// Always check for in-scene placed to assure any previous version scene assets with in-scene place NetworkObjects gets updated
191-
CheckForInScenePlaced();
192-
}
193-
194-
private bool IsEditingPrefab()
195-
{
196-
// Check if we are directly editing the prefab
197-
var stage = PrefabStageUtility.GetPrefabStage(gameObject);
198-
199-
// if we are not editing the prefab directly (or a sub-prefab), then return the object identifier
200-
if (stage == null || stage.assetPath == null)
201-
{
202-
return false;
203-
}
204-
return true;
205296
}
206297

207298
/// <summary>
@@ -212,13 +303,12 @@ private bool IsEditingPrefab()
212303
/// <remarks>
213304
/// This NetworkObject is considered an in-scene placed prefab asset instance if it is:
214305
/// - Part of a prefab
215-
/// - Not being directly edited
216306
/// - Within a valid scene that is part of the scenes in build list
217307
/// (In-scene defined NetworkObjects that are not part of a prefab instance are excluded.)
218308
/// </remarks>
219309
private void CheckForInScenePlaced()
220310
{
221-
if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
311+
if (PrefabUtility.IsPartOfAnyPrefab(this) && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0)
222312
{
223313
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
224314
var assetPath = AssetDatabase.GetAssetPath(prefab);
@@ -231,55 +321,6 @@ private void CheckForInScenePlaced()
231321
IsSceneObject = true;
232322
}
233323
}
234-
235-
private GlobalObjectId GetGlobalId()
236-
{
237-
var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this);
238-
239-
// If not editing a prefab, then just use the generated id
240-
if (!IsEditingPrefab())
241-
{
242-
return instanceGlobalId;
243-
}
244-
245-
// If the asset doesn't exist at the given path, then return the object identifier
246-
var prefabStageAssetPath = PrefabStageUtility.GetPrefabStage(gameObject).assetPath;
247-
// If (for some reason) the asset path is null return the generated id
248-
if (prefabStageAssetPath == null)
249-
{
250-
return instanceGlobalId;
251-
}
252-
253-
var theAsset = AssetDatabase.LoadAssetAtPath<NetworkObject>(prefabStageAssetPath);
254-
// If there is no asset at that path (for some odd/edge case reason), return the generated id
255-
if (theAsset == null)
256-
{
257-
return instanceGlobalId;
258-
}
259-
260-
// If we can't get the asset GUID and/or the file identifier, then return the object identifier
261-
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(theAsset, out var guid, out long localFileId))
262-
{
263-
return instanceGlobalId;
264-
}
265-
266-
// Note: If we reached this point, then we are most likely opening a prefab to edit.
267-
// The instanceGlobalId will be constructed as if it is a scene object, however when it
268-
// is serialized its value will be treated as a file asset (the "why" to the below code).
269-
270-
// Construct an imported asset identifier with the type being a source asset object type
271-
var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, k_SourceAssetObjectType, guid, (ulong)localFileId, 0);
272-
273-
// If we can't parse the result log an error and return the instanceGlobalId
274-
if (!GlobalObjectId.TryParse(prefabGlobalIdText, out var prefabGlobalId))
275-
{
276-
Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})! ** Please Report This Error **");
277-
return instanceGlobalId;
278-
}
279-
280-
// Otherwise, return the constructed identifier for the source prefab asset
281-
return prefabGlobalId;
282-
}
283324
#endif // UNITY_EDITOR
284325

285326
/// <summary>

0 commit comments

Comments
 (0)