edit: Found a workaround for my case ( Posting #2 ), but I’m still more than curious if Unity allows for what I tried to achieve below.
Hello,
I’m working on some savegame related system, where I want to give preplaced objects in the scene a “preplaced” id, so I can sync them with my game state from the savegame.
(I don’t want to just destroy and replace them on loading the savegame)
Problem is: I find no reliable way to savely avoid that the ID component is applied to a prefab if someone in the team hits the apply button on a scene instance with that ID component attached. It’s not just unclean but can also kill the IDs. If clicking apply on two instances in a row, the first one would get the ID of the second one.
2 approaches I tried:
A) First approach:
Use the UnityEditor.PrefabUtility.prefabInstanceUpdated callback and strip the ID component on the instance’s source prefab if it contains one.
But this approach is flawed. What happens:
- Apply => The instance will apply the id component to the prefab
- My code: The id component will be stripped from the prefab
- And as both are in sync now, the id component will also be stripped from the instance… and my code will add a new one with a new id.
Meh.
using System;
using UnityEngine;
public class PreplacedObjectItemLinker : MonoBehaviour
{
// id unique for each preplaced item in the scene
[ReadOnly]
public string prePlacementKey;
#if UNITY_EDITOR
PreplacedObjectItemLinker()
{
UnityEditor.PrefabUtility.prefabInstanceUpdated += PrefabInstanceUpdated;
}
private static void PrefabInstanceUpdated(GameObject root)
{
GameObject prefab = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(root);
if (prefab.GetComponentsInChildren<PreplacedObjectItemLinker>().Length > 0)
{
string assetPath = UnityEditor.AssetDatabase.GetAssetPath(UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(root));
GameObject prefabContent = UnityEditor.PrefabUtility.LoadPrefabContents(assetPath);
foreach (PreplacedObjectItemLinker linker in prefabContent.GetComponentsInChildren<PreplacedObjectItemLinker>())
{
DestroyImmediate(linker, true);
}
UnityEditor.PrefabUtility.SaveAsPrefabAsset(prefabContent, assetPath);
UnityEditor.PrefabUtility.UnloadPrefabContents(prefabContent);
}
}
#endif
}
B) Second approach:
Use HideFlags.
In this case a pretty indirect approach is required.
HideFlags.DontSave will keep a component from being applied to a prefab. Great.
But of course I still want to keep it on my instances when saving a scene.
So I have to reset the flag before scene saving and re-enabe it afterwards.
Works fine so far.
What broke my neck is that HideFlags.DontSave will remove the component when entering play mode. But I need it at runtime.
There is the UnityEditor.EditorApplication.playModeStateChanged callback which I tried to use to reset the HideFlags before entering play mode, but it will trigger too late.
According to this Unity dev there happen already 2 updates before it triggers.
In my case that means: Very often the component is already destroyed before I can reset the hideFlags.
There’s also the [UnityEditor.InitializeOnEnterPlayMode] attribute as an alternative… but methods decorated with it trigger even later.
That everything would work fine in a build (due to #if UNITY_EDITOR) doesn’t help much. :-/
So close and yet so far…
using System;
using UnityEngine;
[ExecuteInEditMode]
public class PreplacedObjectItemLinker : MonoBehaviour
{
// id unique for each preplaced item in the scene
[ReadOnly]
public string prePlacementKey;
#if UNITY_EDITOR
private PreplacedObjectsObserver observerParent;
void Awake()
{
// Don't save this component to a prefab...
hideFlags = HideFlags.DontSave;
// ...but save it on instances when saving the scene
UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (_,_) => { hideFlags = HideFlags.None; };
UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += (_) => { hideFlags = HideFlags.DontSave; };
// Works sometimes. Will often trigger too late.
UnityEditor.EditorApplication.playModeStateChanged += (newPlayMode) =>
{
if (newPlayMode == UnityEditor.PlayModeStateChange.ExitingEditMode)
{
hideFlags = HideFlags.None;
}
else
{
//hideFlags = HideFlags.DontSave;
}
};
// Create ID if needed, resolve ID collisions
// [...]
}
#endif
}
So… long hassle… and still no solution to prevent a component from being applied to prefab from an instance.
Any hint would be largely appreciated.