Monitoring GameObject Changes in the Unity editor hierarchy
Tags: unity3d, gamedev, tooling, csharp
Published: 2024-07-10 08:29:24

### Introduction

Managing GameObjects in Unity can be a complex task, especially when dealing with dynamic and interactive scenes. Detecting changes such as additions, deletions, renaming, and parent changes in the hierarchy is crucial for many applications. In this post, we will walk through a powerful script, `HierarchyMonitor`, which helps in tracking these changes in real-time within the Unity Editor.

### Overview of `HierarchyMonitor`

The `HierarchyMonitor` script utilizes Unity's `EditorApplication.hierarchyChanged` event to monitor changes in the hierarchy. It provides hooks for handling the addition, removal, renaming, and reparenting of GameObjects. 

Here's a breakdown of what the script offers:
- **GameObject Added:** Triggered when a new GameObject is added to the hierarchy.
- **GameObject Removed:** Triggered when an existing GameObject is removed from the hierarchy.
- **GameObject Renamed:** Triggered when a GameObject is renamed.
- **GameObject Parent Changed:** Triggered when a GameObject's parent is changed.

### The Script

Below is the complete script for the `HierarchyMonitor` class:

```csharp
using UnityEditor;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using Object = UnityEngine.Object;

[InitializeOnLoad]
public static class HierarchyMonitor
{
    private static readonly Dictionary<GameObject, string> _previousNames = new();
    private static readonly Dictionary<GameObject, Transform> _previousParents = new();

    public delegate void GameObjectEventHandler(GameObject gameObject);
    public delegate void GameObjectRenamedEventHandler(GameObject editedObject);
    public delegate void GameObjectParentChangedEventHandler(GameObject gameObject, Transform oldParent, Transform newParent);

    public static event GameObjectEventHandler GameObjectAdded;
    public static event GameObjectEventHandler GameObjectRemoved;
    public static event GameObjectRenamedEventHandler GameObjectRenamed;
    public static event GameObjectParentChangedEventHandler GameObjectParentChanged;

    static HierarchyMonitor()
    {
        EditorApplication.hierarchyChanged += OnHierarchyChanged;

        RefreshPreviousState();
    }

    private static void RefreshPreviousState()
    {
        _previousNames.Clear();
        _previousParents.Clear();

        GameObject[] gameObjects = Object.FindObjectsOfType<GameObject>();

        foreach (GameObject gameObject in gameObjects)
        {
            if (gameObject.hideFlags != HideFlags.None ||
                _previousNames.ContainsKey(gameObject))
            {
                continue;
            }

            _previousNames.Add(gameObject, gameObject.name);
            _previousParents.Add(gameObject, gameObject.transform.parent);
        }
    }

    private static void OnHierarchyChanged()
    {
        GameObject[] currentGameObjects = Object.FindObjectsOfType<GameObject>();

        foreach (GameObject gameObject in currentGameObjects)
        {
            HandleGameObjectAdded(gameObject);
            HandleGameObjectRenamed(gameObject);
            HandleGameObjectParentChanged(gameObject);
        }

        ICollection<GameObject> keys = _previousNames.Keys.ToList();

        foreach (GameObject previousGameObject in keys)
        {
            HandleGameObjectDeleted(currentGameObjects, previousGameObject);
        }

        RefreshPreviousState();
    }

    private static void HandleGameObjectDeleted(GameObject[] currentGameObjects, GameObject previousGameObject)
    {
        if (Array.Exists(currentGameObjects, obj => obj == previousGameObject))
        {
            return;
        }

        Debug.Log($"GameObject removed: {_previousNames[previousGameObject]}");
        GameObjectRemoved?.Invoke(previousGameObject);
        _previousNames.Remove(previousGameObject);
    }

    private static void HandleGameObjectRenamed(GameObject gameObject)
    {
        if (!_previousNames.ContainsKey(gameObject) ||
            gameObject.name == _previousNames[gameObject])
        {
            return;
        }

        Debug.Log($"GameObject renamed from: {_previousNames[gameObject]} to: {gameObject.name}");
        GameObjectRenamed?.Invoke(gameObject);
        _previousNames[gameObject] = gameObject.name;
    }

    private static void HandleGameObjectAdded(GameObject gameObject)
    {
        if (_previousNames.ContainsKey(gameObject))
        {
            return;
        }

        Debug.Log($"GameObject added: {gameObject.name}");
        GameObjectAdded?.Invoke(gameObject);
        _previousNames.Add(gameObject, gameObject.name);
        _previousParents.Add(gameObject, gameObject.transform.parent);
    }

    private static void HandleGameObjectParentChanged(GameObject gameObject)
    {
        Transform currentParent = gameObject.transform.parent;

        if (!_previousParents.ContainsKey(gameObject) ||
            currentParent == _previousParents[gameObject])
        {
            return;
        }

        Debug.Log($"GameObject parent changed: {gameObject.name}");
        GameObjectParentChanged?.Invoke(gameObject, _previousParents[gameObject], currentParent);
        _previousParents[gameObject] = currentParent;
    }
}
```

### How to Use the `HierarchyMonitor`

1. **Create a New Script:**
   Create a new C# script in your Unity project and name it `HierarchyMonitor.cs`.

2. **Copy the Script:**
   Copy the provided `HierarchyMonitor` script into the new file.

3. **Place the Script in an Editor Folder:**
   Ensure the script is placed inside an `Editor` folder in your Unity project. This is necessary as it uses `UnityEditor` namespace functions which should only be included in the editor code.

4. **Compile the Script:**
   Unity will automatically compile the script. Once compiled, the `HierarchyMonitor` will start monitoring changes in the hierarchy.

5. **Subscribe to Events:**
   You can now subscribe to the `GameObjectAdded`, `GameObjectRemoved`, `GameObjectRenamed`, and `GameObjectParentChanged` events from any other script to handle these changes as needed.

### Example Usage

Here’s an example of how you can subscribe to these events in another script:

```csharp
using UnityEngine;

public class HierarchyMonitorExample : MonoBehaviour
{
    private void OnEnable()
    {
        HierarchyMonitor.GameObjectAdded += OnGameObjectAdded;
        HierarchyMonitor.GameObjectRemoved += OnGameObjectRemoved;
        HierarchyMonitor.GameObjectRenamed += OnGameObjectRenamed;
        HierarchyMonitor.GameObjectParentChanged += OnGameObjectParentChanged;
    }

    private void OnDisable()
    {
        HierarchyMonitor.GameObjectAdded -= OnGameObjectAdded;
        HierarchyMonitor.GameObjectRemoved -= OnGameObjectRemoved;
        HierarchyMonitor.GameObjectRenamed -= OnGameObjectRenamed;
        HierarchyMonitor.GameObjectParentChanged -= OnGameObjectParentChanged;
    }

    private void OnGameObjectAdded(GameObject gameObject)
    {
        Debug.Log($"Detected GameObject added: {gameObject.name}");
    }

    private void OnGameObjectRemoved(GameObject gameObject)
    {
        Debug.Log($"Detected GameObject removed: {gameObject.name}");
    }

    private void OnGameObjectRenamed(GameObject gameObject)
    {
        Debug.Log($"Detected GameObject renamed: {gameObject.name}");
    }

    private void OnGameObjectParentChanged(GameObject gameObject, Transform oldParent, Transform newParent)
    {
        Debug.Log($"Detected GameObject parent change: {gameObject.name} from {oldParent?.name} to {newParent?.name}");
    }
}
```

### Conclusion

The `HierarchyMonitor` script is a powerful tool for tracking and handling hierarchy changes in the Unity Editor. By utilizing this script, developers can easily detect and respond to changes in the scene, making it an invaluable asset for complex projects. Happy coding!

For the full source code, you can check out the [Gist](https://gist.github.com/WhiteOlivierus/d94b7d88b27250f171865cee06a1bac6).