UniqueID component, but don't apply to prefab

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:

  1. Apply => The instance will apply the id component to the prefab
  2. My code: The id component will be stripped from the prefab
  3. 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.

Well, I got a workaround in place now for the problem I tried to solve, but I’d be still curious how to avoid adding certain components to prefabs.

Workaround for my case:
Instead of saving the IDs in a component for each gameObject, I now do all the ID generation / storing in a parent object.

Essentially:

[ExecuteInEditMode]
public class PreplacedObjectsObserver : MonoBehaviour
{
        // List with info (IDs, et.al) for each preplaced gameObject
        public List<PreplacedItemInfo> preplacedItems = new ();

#if UNITY_EDITOR
        void OnTransformChildrenChanged()
        {
                // Generate/store/update IDs for children here
                // [...]
        }
}
#endif

As the parent object isn’t a prefab… problem solved.

Instead of stripping the component from the prefab in the first approach, could you instead set the ID to null / empty on the base prefab and not on the instances (in a custom editor maybe)?

//Is this a prefab instance?
PrefabUtility.GetPrefabInstanceStatus(obj) != PrefabInstanceStatus.NotAPrefab;

//Is this a prefab?
PrefabUtility.GetPrefabAssetType(obj) != PrefabAssetType.NotAPrefab;

I’m also very interested in this. It seems to be something everyone has tried to solve at one point or another, with varying degrees of success!

1 Like

Hm. I don’t think you can use the prefabInstanceUpdated callback to solve this problem.

Example: You got instance A and instance B.
If I hit “Apply All” on instance B it could very well happen that instance A is the first where the callback is triggered.
And it triggers AFTER the changes from B were applied to the prefab and propagated to A.
That means A’s original information is already gone.

Theoretically it could be possible to save the other instances’s information if the instance where we click the “Apply All” button is also the first to receive the callback… but when I just tested it, it seemed to be more or less random.

1 Like

How about this approach of using the ISerializationCallbackReceiver to intercept the save process?

1 Like

I actually played around with OnBeforeSerialize() just today… but didn’t think about that.

Though I only see one theoretical way to make it work:
To have ANOTHER component on the prefab check if it is about to get serialized - the ID component can’t do it as the whole point is that the prefab should be kept clean of the ID component.
That other “guard” component could then try to quickly enable HideFlags.DontSave on all ID components in the scene and reset them again OnAfterSerialize().

Not sure though if that isn’t already too late and I doubt you want to place that guard component on every prefab.
Another flaw is that OnBeforeSerialize() is called super-often, even if seemingly nothing happens.

Am I wrong?

I think I was working with the assumption that the prefab would keep the ID component, but just not save a unique ID. So each instance in a scene would propagate that field, but the prefab ID would just remain null / 0 / “”. Instances in scenes that already have an ID would ignore the update (isn’t this already the case with prefabs? An overridden field is not automatically updated when hitting “Apply All”?)

That worked for me in the past just using a custom editor, but this was before the new prefab workflow which I know has added some extra hoops to jump through.

1 Like