Search Unity

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

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    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.
     
  2. MadWatch

    MadWatch

    Joined:
    May 26, 2016
    Posts:
    112
    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

    florianBrn

    Joined:
    Jul 31, 2019
    Posts:
    53
    Bump, as this would be very useful!
     
  4. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
  5. Xelnath

    Xelnath

    Joined:
    Jan 31, 2015
    Posts:
    402
  6. Xelnath

    Xelnath

    Joined:
    Jan 31, 2015
    Posts:
    402
  7. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
    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. }
    93.  
    https://gist.github.com/karljj1/f8adaf0024288052f991191777110f3d
     
    Last edited: Jul 8, 2022
    IAVAR, Ghosthowl, highpockets and 4 others like this.
  8. Xelnath

    Xelnath

    Joined:
    Jan 31, 2015
    Posts:
    402
    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

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
    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

    rafaelrbenavent

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

    highpockets

    Joined:
    Jan 17, 2013
    Posts:
    71
    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

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
    You should look at the link version instead. I updated it https://gist.github.com/karljj1/f8adaf0024288052f991191777110f3d

    The updated version is also in the docs now https://docs.unity3d.com/2023.1/Doc...ence/ObjectChangeEvents-changesPublished.html
     
    Alverik and highpockets like this.
  13. Canijo

    Canijo

    Joined:
    Oct 9, 2018
    Posts:
    50
    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

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
    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.
     
    funkyCoty and Canijo like this.
  15. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
  16. TomJerbo

    TomJerbo

    Joined:
    Oct 11, 2019
    Posts:
    2
    Hi, I removed my comment when I thought I had found an answer, using the same callback that you linked.
    It's detecting when something is made but I don't know how to detect if it was duplicated or just created normally.

    It says that "importedAssets contains paths of all assets used in the operation." but I'm only getting a path to the object that was created and not the original.
    Any tips on where to look?
     
  17. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
    There's no simple way to detect asset duplication however I have seen people use OnWillCreateAsset to do this.
    It won't tell you if the new asset is a duplicate, but it will fire just before the asset is created and I guess you can implement some logic that can filter on assets that already exist or something similar.
     
  18. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    As someone who nearly exclusively makes editor tools, this is pretty huge. I spend hours sometimes digging through assemblies and packages for any little odds and ends that might be helpful/useful. How could I have missed this? Thanks a ton for sharing this.
     
    Xelnath and karl_jones like this.
  19. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
  20. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,299
    What do you mean? We don't have access to that at runtime either. Most of these events don't happen outside of the editor, they are either part of the asset database or the Undo system, and both are editor-only.

    If you look at the docs you will see that most of these events come from the Undo system. https://docs.unity3d.com/ScriptReference/ObjectChangeKind.html
     
    spiney199 likes this.