Skip to content

fix: GlobalObjectIdHash generation for already existing in-scene placed prefab instances [MTT-7055] #2707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 99 additions & 12 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,53 @@ public uint PrefabIdHash
#if UNITY_EDITOR
private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}";

/// <summary>
/// Object Types <see href="https://docs.unity3d.com/ScriptReference/GlobalObjectId.html"/>
/// </summary>
// 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<NetworkObject>(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();
Expand All @@ -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;
}
Expand All @@ -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<NetworkObject>(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<NetworkObject>(prefabStageAssetPath);
// If there is no asset at that path (for some odd/edge case reason), return the generated id
if (theAsset == null)
{
return instanceGlobalId;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
internal class NetworkObjectRefreshTool
{
private static List<string> s_ScenesToUpdate = new List<string>();
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down