I’m looking for a callback to detect when an object is instantiated, or created as the result of pasting, in the editor. Anyone have ideas?
My specific case is that I’ve got a string field on a script that I use as an ID for serialization/deserialization:
string id = UniqueID();
string UniqueID() {
// do random ID generation
return newID;
}
However, if I copy/paste a GameObject with the script, the new instance will have the same ID. Since I use the ID to locate objects during Save/Load, that’s a problem. If I could get a callback whenever a new version of the script in instantiated in the editor, I could catch this and assign the new instance a new UniqueID.
Anyone know a callback that could work? Or if you know a better approach to this problem, I’d love to know that too.
Thanks-
O
Forget the callback and just use Unity’s instance ID.
B)
Call UniqueID on Awake, use [ExecuteInEditMode], track currently used Id’s with a static Dictionary<string,MyClass> and wrap it all in #if UNITY_EDITOR defines so there’s no deployment runtime overhead as all id’s will be assigned and restored using Unity’s internal serialization process.
I’d use option A because I’m lazy and less code = less chance of it breaking.
I know this is old, but just wanted to chime in and say option A isn’t a good idea generally. The instance ID is only unique for the session and does not persist.
The name “Reset” is kind of odd, but the reasoning behind it is that it gets called whenever the component’s default values are loaded - which is at the time of creation in the editor or when you click “reset” on the component.
Yup, the closest i can get is with a combination of Reset and OnValidate (below). It’s close, but it’s seeming like an impossibility to truly discern a duplicate/paste from the hierarchy context menu as it just doesn’t trigger anything with enough specificity. Maybe a Dev can chime in…
#if (UNITY_EDITOR)
//Works for: AddComponent, Component Context Menu 'Reset', Component Context Menu 'Paste Component As New'
private void Reset() {
if (Event.current != null) {
Debug.Log("Reset - Type: " + Event.current.type.ToString());
} else {
//Component Context Menu 'Paste Component As New'
Debug.Log("Reset - EventSystem: null");
}
}
private void OnValidate() {
if (Event.current != null) {
if (Event.current.type == EventType.ExecuteCommand && Event.current.commandName == "Duplicate") {
//Ctrl+D
//Main Menu 'Edit > Duplicate'
Debug.Log("ExecuteCommand - Duplicate");
} else if (Event.current.type == EventType.ExecuteCommand && Event.current.commandName == "Paste") {
//Ctrl+V
//Main Menu 'Edit > Paste'
Debug.Log("ExecuteCommand - Paste");
} else if (Event.current.type == EventType.ValidateCommand && Event.current.commandName == "Paste") {
//Never seems to occur...
Debug.Log("ValidateCommand - Paste");
} else if (Event.current.type == EventType.ContextClick) {
//Never seems to occur...
Debug.Log("ContextClick - Indeterminable");
} else if (Event.current.type == EventType.KeyDown && (Event.current.modifiers == EventModifiers.Control && Event.current.keyCode == KeyCode.Z)) {
//Ctrl+Z
Debug.Log("KeyDown - Undo");
} else if (Event.current.type == EventType.KeyDown && (Event.current.modifiers == EventModifiers.Control && Event.current.keyCode == KeyCode.Y)) {
//Ctrl+Y
Debug.Log("KeyDown - Redo");
} else if (Event.current.type == EventType.MouseUp) {
//Main Menu 'Component > Add...'
//Inspector 'Add Component'
Debug.Log("MouseUp - Unknown Specificity.");
} else {
Debug.LogError("Unhandled Type: " + Event.current.type.ToString());
}
} else {
//Save Edited Script... (Re-compile)
//Hierarchy Context Menu 'Paste'
//Hierarchy Context Menu 'Duplicate'
//Component Context Menu 'Paste Component As New' <- Captured by Reset with indeterminable specificity too
//Main Menu 'Edit > Undo'
//Main Menu 'Edit > Redo'
//Debug.LogError("Indeterminable Specificity");
}
}
#endif
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public class ObjectChangeEventsExample
{
static ObjectChangeEventsExample()
{
ObjectChangeEvents.changesPublished += ChangesPublished;
}
static void ChangesPublished(ref ObjectChangeEventStream stream)
{
for (int i = 0; i < stream.length; ++i)
{
var type = stream.GetEventType(i);
switch (type)
{
case ObjectChangeKind.ChangeScene:
stream.GetChangeSceneEvent(i, out var changeSceneEvent);
Debug.Log($"{type}: {changeSceneEvent.scene}");
break;
case ObjectChangeKind.CreateGameObjectHierarchy:
stream.GetCreateGameObjectHierarchyEvent(i, out var createGameObjectHierarchyEvent);
var newGameObject = EditorUtility.InstanceIDToObject(createGameObjectHierarchyEvent.instanceId) as GameObject;
Debug.Log($"{type}: {newGameObject} in scene {createGameObjectHierarchyEvent.scene}.");
break;
case ObjectChangeKind.ChangeGameObjectStructureHierarchy:
stream.GetChangeGameObjectStructureHierarchyEvent(i, out var changeGameObjectStructureHierarchy);
var gameObject = EditorUtility.InstanceIDToObject(changeGameObjectStructureHierarchy.instanceId) as GameObject;
Debug.Log($"{type}: {gameObject} in scene {changeGameObjectStructureHierarchy.scene}.");
break;
case ObjectChangeKind.ChangeGameObjectStructure:
stream.GetChangeGameObjectStructureEvent(i, out var changeGameObjectStructure);
var gameObjectStructure = EditorUtility.InstanceIDToObject(changeGameObjectStructure.instanceId) as GameObject;
Debug.Log($"{type}: {gameObjectStructure} in scene {changeGameObjectStructure.scene}.");
break;
case ObjectChangeKind.ChangeGameObjectParent:
stream.GetChangeGameObjectParentEvent(i, out var changeGameObjectParent);
var gameObjectChanged = EditorUtility.InstanceIDToObject(changeGameObjectParent.instanceId) as GameObject;
var newParentGo = EditorUtility.InstanceIDToObject(changeGameObjectParent.newParentInstanceId) as GameObject;
var previousParentGo = EditorUtility.InstanceIDToObject(changeGameObjectParent.previousParentInstanceId) as GameObject;
Debug.Log($"{type}: {gameObjectChanged} from {previousParentGo} to {newParentGo} from scene {changeGameObjectParent.previousScene} to scene {changeGameObjectParent.newScene}.");
break;
case ObjectChangeKind.ChangeGameObjectOrComponentProperties:
stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out var changeGameObjectOrComponent);
var goOrComponent = EditorUtility.InstanceIDToObject(changeGameObjectOrComponent.instanceId);
if (goOrComponent is GameObject go)
{
Debug.Log($"{type}: GameObject {go} change properties in scene {changeGameObjectOrComponent.scene}.");
}
else if (goOrComponent is Component component)
{
Debug.Log($"{type}: Component {component} change properties in scene {changeGameObjectOrComponent.scene}.");
}
break;
case ObjectChangeKind.DestroyGameObjectHierarchy:
stream.GetDestroyGameObjectHierarchyEvent(i, out var destroyGameObjectHierarchyEvent);
// The destroyed GameObject can not be converted with EditorUtility.InstanceIDToObject as it has already been destroyed.
var destroyParentGo = EditorUtility.InstanceIDToObject(destroyGameObjectHierarchyEvent.parentInstanceId) as GameObject;
Debug.Log($"{type}: {destroyGameObjectHierarchyEvent.instanceId} with parent {destroyParentGo} in scene {destroyGameObjectHierarchyEvent.scene}.");
break;
case ObjectChangeKind.CreateAssetObject:
stream.GetCreateAssetObjectEvent(i, out var createAssetObjectEvent);
var createdAsset = EditorUtility.InstanceIDToObject(createAssetObjectEvent.instanceId);
var createdAssetPath = AssetDatabase.GUIDToAssetPath(createAssetObjectEvent.guid);
Debug.Log($"{type}: {createdAsset} at {createdAssetPath} in scene {createAssetObjectEvent.scene}.");
break;
case ObjectChangeKind.DestroyAssetObject:
stream.GetDestroyAssetObjectEvent(i, out var destroyAssetObjectEvent);
// The destroyed asset can not be converted with EditorUtility.InstanceIDToObject as it has already been destroyed.
Debug.Log($"{type}: Instance Id {destroyAssetObjectEvent.instanceId} with Guid {destroyAssetObjectEvent.guid} in scene {destroyAssetObjectEvent.scene}.");
break;
case ObjectChangeKind.ChangeAssetObjectProperties:
stream.GetChangeAssetObjectPropertiesEvent(i, out var changeAssetObjectPropertiesEvent);
var changeAsset = EditorUtility.InstanceIDToObject(changeAssetObjectPropertiesEvent.instanceId);
var changeAssetPath = AssetDatabase.GUIDToAssetPath(changeAssetObjectPropertiesEvent.guid);
Debug.Log($"{type}: {changeAsset} at {changeAssetPath} in scene {changeAssetObjectPropertiesEvent.scene}.");
break;
case ObjectChangeKind.UpdatePrefabInstances:
stream.GetUpdatePrefabInstancesEvent(i, out var updatePrefabInstancesEvent);
string s = "";
s += $"{type}: scene {updatePrefabInstancesEvent.scene}. Instances ({updatePrefabInstancesEvent.instanceIds.Length}):\n";
foreach (var prefabId in updatePrefabInstancesEvent.instanceIds)
{
s += EditorUtility.InstanceIDToObject(prefabId).ToString() + "\n";
}
Debug.Log(s);
break;
}
}
}
}
Just so that anyone reading this is aware, I spent 4 hours battling this API to get it to do the things I want. There are lots of basic design flaws, like the fact that you can’t tell if a Component was added/removed from a ChangeGameObjectStructure, you can’t do GetComponent or any analysis on a deleted GameObject (because it’s already gone by the time you receive the event), and several of the stream events provide null data in unexpected scenarios when they really shouldn’t, like when you Undo a GameObject creation before you finish typing the name in.
I couldn’t figure this out, i’d instantiate one item and i just had some logic in “CreateGameObjectHierarchy” but it’d be called 200 times. Is this because it’s running a for loop? Is there another way to do it?
What a strange system, they didn’t even put any implementation instructions in the API lol
Here is my code from an old project which handles a lot of the edge cases resulting from the event stream. The intent is to keep an up-to-date Editor list of RoomDefinitions which each have their own lists of specified components present in child objects. There are still some unknown issues which cause the list to occasionally go out of sync, so I end up needing to manually re-sync it once in a while. Maybe someone else has better code which handles 100% of edge cases.
[InitializeOnLoadMethod]
private static void Initialize()
{
ObjectChangeEvents.changesPublished += ChangesPublished;
}
private static void ChangesPublished(ref ObjectChangeEventStream eventStream)
{
for (int i = 0; i < eventStream.length; ++i)
{
var type = eventStream.GetEventType(i);
switch (type)
{
case ObjectChangeKind.ChangeGameObjectStructure:
{
eventStream.GetChangeGameObjectStructureEvent(i, out var changeGameObjData);
var changedGameObject = EditorUtility.InstanceIDToObject(changeGameObjData.instanceId) as GameObject;
//null happens when you Undo a freshly-created object without naming it
if (changedGameObject == null || EditorSceneManager.IsPreviewScene(changedGameObject.scene))
return;
RoomDefinition roomDefinition = changedGameObject.GetComponent<RoomDefinition>();
//this means the RoomDefinition component was just added
if (roomDefinition != null && !roomList.Contains(roomDefinition))
{
Debug.Log("Room was added");
roomList.Add(roomDefinition);
InitRoom(roomDefinition);
return;
}
if (changedGameObject.CompareTag(TagRegistry.exemptTag))
return;
RoomDefinition parentRoom = SearchForParentRoom(changedGameObject.transform);
if (parentRoom == null)
return;
bool didAddChildren = AddComponentsOfInterestToRoomDefinitionLists(changedGameObject, parentRoom);
//need to check if any list elements are null in case a MonoB of interest was removed (we have no way of finding out if it was an AddComponent or Destroy)
if (didAddChildren == false)
RemoveNullReferencesInList(parentRoom);
break;
}
case ObjectChangeKind.ChangeGameObjectParent:
{
eventStream.GetChangeGameObjectParentEvent(i, out var changeGameObjectParentData);
GameObject oldParent = EditorUtility.InstanceIDToObject(changeGameObjectParentData.previousParentInstanceId) as GameObject;
GameObject newParent = EditorUtility.InstanceIDToObject(changeGameObjectParentData.newParentInstanceId) as GameObject;
GameObject movedGameObject = EditorUtility.InstanceIDToObject(changeGameObjectParentData.instanceId) as GameObject;
RoomDefinition oldParentRoom = oldParent == null ? null : SearchForParentRoom(oldParent.transform);
RoomDefinition newParentRoom = newParent == null ? null : SearchForParentRoom(newParent.transform);
//not under any RoomDefinition before or after
//This is also triggered by a Unity bug if you Undo a move to the scene root
if (oldParentRoom == null && newParentRoom == null)
{
//due to the bug, movedGameObject is also null, so we need to use Selection.activeTransform, since Undo selects the undone object
movedGameObject = Selection.activeTransform.gameObject;
newParentRoom = SearchForParentRoom(movedGameObject == null ? null : movedGameObject.transform);
//if parent room is still null, then it means there were genuinely no parent rooms. So don't proceed
if (newParentRoom == null)
return;
}
//GameObject was not under a RoomDefinition previously, so just add to new roomdef
if (oldParentRoom == null)
AddComponentsOfInterestToRoomDefinitionLists(movedGameObject, newParentRoom);
//GameObject was moved from parentage under a RoomDefinition to outside of a RoomDefinition
else if (newParentRoom == null)
RemoveGameObjectFromRoomDefinitionList(movedGameObject, oldParentRoom);
//Move from old parent room to new parent room
else if (oldParentRoom != newParent)
{
//this could be made slightly more efficient by returning a list of removed CullableComponents from the remove function to avoid GetComponents in the add function
RemoveGameObjectFromRoomDefinitionList(movedGameObject, oldParentRoom);
AddComponentsOfInterestToRoomDefinitionLists(movedGameObject, newParentRoom);
}
break;
}
case ObjectChangeKind.DestroyGameObjectHierarchy:
{
eventStream.GetDestroyGameObjectHierarchyEvent(i, out var destroyGameObjData);
var destroyedGameObjectParent = EditorUtility.InstanceIDToObject(destroyGameObjData.parentInstanceId) as GameObject;
Transform transformToSearchFrom = null;
//destroyedGameObjectParent is null when you Undo a GO creation, so we have to use Selection
if (destroyedGameObjectParent == null || EditorSceneManager.IsPreviewScene(destroyedGameObjectParent.scene))
transformToSearchFrom = Selection.activeTransform;
else
transformToSearchFrom = destroyedGameObjectParent.transform;
//this can trigger in rare circumstances
if (transformToSearchFrom == null)
return;
//can't check which component was destroyed because the GameObject is completely gone, so have to manually check all of them
RoomDefinition parentRoom = SearchForParentRoom(transformToSearchFrom);
if (parentRoom != null)
RemoveNullReferencesInList(parentRoom);
break;
}
case ObjectChangeKind.CreateGameObjectHierarchy:
{
eventStream.GetCreateGameObjectHierarchyEvent(i, out var createGameObjData);
var createdGameObject = EditorUtility.InstanceIDToObject(createGameObjData.instanceId) as GameObject;
//this is null while in Prefab mode (Preview scene). Leaving the null check for any future unexpected cases
if (createdGameObject == null || EditorSceneManager.IsPreviewScene(createdGameObject.scene))
return;
if (createdGameObject.CompareTag(TagRegistry.exemptTag))
return;
RoomDefinition parentRoom = SearchForParentRoom(createdGameObject.transform);
if (parentRoom == null)
return;
AddComponentsOfInterestToRoomDefinitionLists(createdGameObject, parentRoom);
//ObjectChangeKind.CreateGameObjectHierarchy does not get triggered for the child objects in the event of a duplication, so have to manually do it
AddChildrenComponentsOfInterest(createdGameObject, parentRoom);
break;
}
//check for tag changed to/from exempt tag
case ObjectChangeKind.ChangeGameObjectOrComponentProperties:
{
eventStream.GetChangeGameObjectOrComponentPropertiesEvent(i, out var changeGameObjOrComponentPropertiesData);
var changedGameObject = EditorUtility.InstanceIDToObject(changeGameObjOrComponentPropertiesData.instanceId) as GameObject;
if (changedGameObject == null)
return;
bool shouldRemoveFromRoom = changedGameObject.CompareTag(TagRegistry.exemptTag);
RoomDefinition parentRoom = SearchForParentRoom(changedGameObject.transform);
if (parentRoom == null)
return;
if (shouldRemoveFromRoom)
RemoveGameObjectFromRoomDefinitionList(changedGameObject, parentRoom);
else
AddComponentsOfInterestToRoomDefinitionLists(changedGameObject, parentRoom);
break;
}
}
}
}
I solved this by also storing the Unity Instance ID to a serialized variable so I can detect if this GameObject is a copy and then create a new unique ID for it:
[ExecuteAlways]
public class Location : MonoBehaviour
{
[SerializeField] private string m_id;
[HideInInspector]
[SerializeField] private int m_instanceId;
private void Update()
{
if (Application.isPlaying == false && (string.IsNullOrEmpty(m_id) || m_instanceId != GetInstanceID()))
{
m_id = Guid.NewGuid().ToString();
m_instanceId = GetInstanceID();
}
}
}
Instance IDs change every time the editor opens anew, as well as when running a build. Therefore I wouldn’t serialize the GUID into the MonoBehaviour because that’ll bloat your scene and every time you open the project, all GameObjects will be considered “a copy” (have you tested that?). Ideally you will want to save that GUID someplace else, outside the Assets folder.
And then you should keep the GUID itself, rather than unnecessarily change it into a string. To test if the GUID is valid use Guid.IsEmpty. A GUID string is going to waste performance and will increase the size of the serialized data (and thus the scene) because a string GUID expands to more bytes (eg an int is 4 bytes but the value it holds, for example 1234567890 is 10 bytes or even 20+ bytes if it’s a unicode string).
Lastly, this script will run at runtime and call its Update method. Even though it merely does a simply bool check that’ll be false, even an empty Update method has some overhead and I assume this script will possibly be on hundreds of Game Objects. Thus add an Awake method that calls Destroy(this) when not in the editor and preferably enclose the Update method (not just the body) within #if UNITY_EDITOR.
I’m using these gameobjects as spawnpoints and I need to identify them uniquely as the same GUID is serialized (as JSON for now) separately, that’s why it’s stored as a string here. I have no performance considerations with this particular implementation. Having the GUID as a string makes it easy to debug this also.
That aside, the actual problem is to automatically generated the unique ID and have Unity serialize it for me. This works, but not when I copy-paste objects. You are correct about the Instance IDs, I’ll have to ditch that plan. I’ll probably make a small custom editor for these objects to generate and verify the IDs.
Ideally Unity would call Reset() for all newly generated gameobjects, but unfortunately that’s not the case for copypasted ones.