Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

Feedback Editor callbacks for GameObject creation, deletion, duplication by user or user script

Discussion in 'Editor & General Support' started by Xarbrough, Dec 5, 2019.

  1. Xarbrough


    Dec 11, 2014
    As someone who writes a lot of editor tools, I often would like to know when a GameObject in the scene was created, deleted or duplicated by the user. More specifically I would like to cover the following cases:
    • GameObject is created and placed in the scene
    • GameObject is deleted and removed from the scene
    • GameObject is has been duplicated (probably needed after the creation callback for the same object)
    These events can be invoked by multiple inputs:
    • Keyboard shortcuts
    • Context menus
    • User editor scripts
    For this to work, there must be a unified system through which all objects in Unity are handled: The Undo system. This seems to be the crucial point. I don't want to receive a callback from the engine side when an object is allocated or loaded, because this is also triggered when the scene is closed or play mode entered/exited. Instead, I would like to receive events that have user intent. The Undo system seems like the perfect fit for this case, since undoable actions are everything that the user actively controls.

    Here is a quick rundown of my past research and experiences and why I believe that the existing options do not suffice:

    MonoBehaviour ExecuteInEditMode calls Awake and OnDestroy, but this happens whenever the object is loaded or goes out of scope, for example, when the scene is closed. Handling unnecessary callbacks results in a lot of boilerplate code and is always error prone to accidentally setting the scene dirty unintentionally.

    IMGUI CommandEvent, e.g. polling the GUI loop for event names such as "SoftDelete", however these do not happen when a user uses the ContextMenu.

    Trying to track the active selection in combination with certain commands, comparing instance IDs, etc: While some users have success up to a certain degree by trying to interpret several sources of information, these methods often fall short at some point, e.g. when the inspector is locked or multiple windows are open.

    Undo.postprocessModifications goes into a nice direction, but it only provides feedback for changes made to properties on objects, but not about their lifetime, no callback when a GameObject is created, duplicated or deleted.

    EditorApplication.hierarchyChanged seems like another good options for a unified event, but it doesn't provide any information, so it would be up to me, to parse the entire scene and figure out what has changed, which seems like a lot of work and overhead for something that Unity already does under the hood.
    alexvector and sl1nk3_ubi like this.
  2. MadWatch


    May 26, 2016
    Seconded. That would be very useful.

    Also, I noticed that Undo.postprocessModifications is called when an object is created, but isn't called when an object is created by draping and dropping an asset into the scene view (eg: dragging an dropping a sprite on the scene view to create a new GameObject with a SpriteRenderer). Is this a bug or is this by design?
  3. florianBrn


    Jul 31, 2019
    Bump, as this would be very useful!
  4. karl_jones


    Unity Technologies

    May 5, 2015
  5. Xelnath


    Jan 31, 2015
  6. Xelnath


    Jan 31, 2015
  7. karl_jones


    Unity Technologies

    May 5, 2015
    Yes, the documentation is poor for this. I'll try and improve it next week. For now, here is an example to show how to use each event

    Code (csharp):
    1. using System.Text;
    2. using UnityEditor;
    3. using UnityEngine;
    4. [InitializeOnLoad]
    5. public class ObjectChangeEventsExample
    6. {
    7.     static ObjectChangeEventsExample()
    8.     {
    9.         ObjectChangeEvents.changesPublished += ChangesPublished;
    10.     }
    11.     static void ChangesPublished(ref ObjectChangeEventStream stream)
    12.     {
    13.         for (int i = 0; i < stream.length; ++i)
    14.         {
    15.             switch (stream.GetEventType(i))
    16.             {
    17.                 case ObjectChangeKind.ChangeScene:
    18.                     stream.GetChangeSceneEvent(i, out var changeSceneEvent);
    19.                     Debug.Log($"Change Scene Event: {changeSceneEvent.scene}");
    20.                     break;
    21.                 case ObjectChangeKind.CreateGameObjectHierarchy:
    22.                     stream.GetCreateGameObjectHierarchyEvent(i, out var createGameObjectHierarchyEvent);
    23.                     var newGameObject = EditorUtility.InstanceIDToObject(createGameObjectHierarchyEvent.instanceId) as GameObject;
    24.                     Debug.Log($"Create GameObject: {newGameObject} in scene {createGameObjectHierarchyEvent.scene}.");
    25.                     break;
    26.                 case ObjectChangeKind.ChangeGameObjectStructureHierarchy:
    27.                     stream.GetChangeGameObjectStructureHierarchyEvent(i, out var changeGameObjectStructureHierarchy);
    28.                     var gameObject = EditorUtility.InstanceIDToObject(changeGameObjectStructureHierarchy.instanceId) as GameObject;
    29.                     Debug.Log($"Change GameObject hierarchy: {gameObject} in scene {changeGameObjectStructureHierarchy.scene}.");
    30.                     break;
    31.                 case ObjectChangeKind.ChangeGameObjectStructure:
    32.                     stream.GetChangeGameObjectStructureEvent(i, out var changeGameObjectStructure);
    33.                     var gameObjectStructure = EditorUtility.InstanceIDToObject(changeGameObjectStructure.instanceId) as GameObject;
    34.                     Debug.Log($"Change GameObject structure: {gameObjectStructure} in scene {changeGameObjectStructure.scene}.");
    35.                     break;
    36.                 case ObjectChangeKind.ChangeGameObjectParent:
    37.                     stream.GetChangeGameObjectParentEvent(i, out var changeGameObjectParent);
    38.                     var gameObjectChanged = EditorUtility.InstanceIDToObject(changeGameObjectParent.instanceId) as GameObject;
    39.                     var newParentGo = EditorUtility.InstanceIDToObject(changeGameObjectParent.newParentInstanceId) as GameObject;
    40.                     var previousParentGo = EditorUtility.InstanceIDToObject(changeGameObjectParent.previousParentInstanceId) as GameObject;
    41.                     Debug.Log($"GameObject change parent from {previousParentGo} to {newParentGo} from scene {changeGameObjectParent.previousScene} to scene {changeGameObjectParent.newScene}.");
    42.                     break;
    43.                 case ObjectChangeKind.ChangeGameObjectOrComponentProperties:
    44.                     stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out var changeGameObjectOrComponent);
    45.                     var goOrComponent = EditorUtility.InstanceIDToObject(changeGameObjectOrComponent.instanceId);
    46.                     if (goOrComponent is GameObject go)
    47.                     {
    48.                         Debug.Log($"GameObject {go} change properties in scene {changeGameObjectOrComponent.scene}.");
    49.                     }
    50.                     else if (goOrComponent is Component component)
    51.                     {
    52.                         Debug.Log($"Component {component} change properties in scene {changeGameObjectOrComponent.scene}.");
    53.                     }
    54.                     break;
    55.                 case ObjectChangeKind.DestroyGameObjectHierarchy:
    56.                     stream.GetDestroyGameObjectHierarchyEvent(i, out var destroyGameObjectHierarchyEvent);
    57.                     var destroyGo = EditorUtility.InstanceIDToObject(destroyGameObjectHierarchyEvent.instanceId) as GameObject;
    58.                     var destroyParentGo = EditorUtility.InstanceIDToObject(destroyGameObjectHierarchyEvent.parentInstanceId) as GameObject;
    59.                     Debug.Log($"Destroy GameObject hierarchy. GameObject: {destroyGo} with parent {destroyParentGo} in scene {destroyGameObjectHierarchyEvent.scene}.");
    60.                     break;
    61.                 case ObjectChangeKind.CreateAssetObject:
    62.                     stream.GetCreateAssetObjectEvent(i, out var createAssetObjectEvent);
    63.                     var createdAsset = EditorUtility.InstanceIDToObject(createAssetObjectEvent.instanceId);
    64.                     var createdAssetPath = AssetDatabase.GUIDToAssetPath(createAssetObjectEvent.guid);
    65.                     Debug.Log($"Created asset {createdAsset} at {createdAssetPath} in scene {createAssetObjectEvent.scene}.");
    66.                     break;
    67.                 case ObjectChangeKind.DestroyAssetObject:
    68.                     stream.GetDestroyAssetObjectEvent(i, out var destroyAssetObjectEvent);
    69.                     var destroyAsset = EditorUtility.InstanceIDToObject(destroyAssetObjectEvent.instanceId);
    70.                     var destroyAssetPath = AssetDatabase.GUIDToAssetPath(destroyAssetObjectEvent.guid);
    71.                     Debug.Log($"Destroy asset {destroyAsset} at {destroyAssetPath} in scene {destroyAssetObjectEvent.scene}.");
    72.                     break;
    73.                 case ObjectChangeKind.ChangeAssetObjectProperties:
    74.                     stream.GetChangeAssetObjectPropertiesEvent(i, out var changeAssetObjectPropertiesEvent);
    75.                     var changeAsset = EditorUtility.InstanceIDToObject(changeAssetObjectPropertiesEvent.instanceId);
    76.                     var changeAssetPath = AssetDatabase.GUIDToAssetPath(changeAssetObjectPropertiesEvent.guid);
    77.                     Debug.Log($"Change asset {changeAsset} at {changeAssetPath} in scene {changeAssetObjectPropertiesEvent.scene}.");
    78.                     break;
    79.                 case ObjectChangeKind.UpdatePrefabInstances:
    80.                     stream.GetUpdatePrefabInstancesEvent(i, out var updatePrefabInstancesEvent);
    81.                     var ss = new StringBuilder();
    82.                     ss.AppendLine($"Update Prefabs in scene {updatePrefabInstancesEvent.scene}");
    83.                     foreach (var prefabId in updatePrefabInstancesEvent.instanceIds)
    84.                     {
    85.                         ss.AppendLine(EditorUtility.InstanceIDToObject(prefabId).ToString());
    86.                     }
    87.                     Debug.Log(ss.ToString());
    88.                     break;
    89.             }
    90.         }
    91.     }
    92. }
    Last edited: Jul 8, 2022
  8. Xelnath


    Jan 31, 2015
    Now that's helpful! You could probably paste that code sample into the function documentation and save a lot of headaches for future devs.
    karl_jones likes this.
  9. karl_jones


    Unity Technologies

    May 5, 2015
    That's the plan ;)
    I'll give it a little cleanup and update it Monday although it takes a little longer to filter through to the website.
    Xelnath likes this.
  10. rafaelrbenavent


    Sep 29, 2018
    @karl_jones the "Asset" ObjectChangeKind do not seem to work for ScriptableObjects.
  11. highpockets


    Jan 17, 2013
    This stream is great, however, this snippet here does not work because the GameObject is Null at this point:

    Code (CSharp):
    1. case ObjectChangeKind.DestroyAssetObject:
    2.     stream.GetDestroyAssetObjectEvent(i, out var destroyAssetObjectEvent);
    3.     var destroyAsset = EditorUtility.InstanceIDToObject(destroyAssetObjectEvent.instanceId);
    4.     var destroyAssetPath = AssetDatabase.GUIDToAssetPath(destroyAssetObjectEvent.guid);
    5.     Debug.Log($"Destroy asset {destroyAsset} at {destroyAssetPath} in scene {destroyAssetObjectEvent.scene}.");
    6.     break;
  12. karl_jones


    Unity Technologies

    May 5, 2015
    You should look at the link version instead. I updated it

    The updated version is also in the docs now
    Alverik and highpockets like this.
  13. Canijo


    Oct 9, 2018
    Does it make sense to Push our own events through the ObjectChangeEventStream.Builder struct?

    I mean, does any Unity's internal tools actually listen to this Stream to react? Why is it public?
  14. karl_jones


    Unity Technologies

    May 5, 2015
    How would you push your own events? There's no public API for this.

    We do use it internally and it's used by some of our packages.
    I don't know the details about why this particular API was added but it seems useful for anyone building tooling that needs to know when a change occurs. We used it in the localization package for this.
    Canijo likes this.