NEW: Reference Dependency View integrated inside Hiearchy View

Unity being a reference centric engine WITHOUT a reference graph visualizer, it’s painful when you debug a rig after a month working on something else. Unreal has the reference graph but it’s not well integrated with other windows. So I made this thing, which I already find super useful even in its highly unoptimized form. It draws reference lines within the hierarchy view.

Since this new forum doesn’t do embed mp4 anymore, here’s the twit with the video.

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor.Experimental.SceneManagement;

[InitializeOnLoad]
public class HierarachyReferenceVisualizer
{
	static GameObject           selectedObject;
	static HashSet<GameObject>  referencedObjects  = new HashSet<GameObject>();
	static HashSet<GameObject>  referencingObjects = new HashSet<GameObject>();
	static Dictionary<int,Rect> itemRects          = new Dictionary<int,Rect>();
	static Rect                 selectedRect;

	static HierarachyReferenceVisualizer()
	{
		Selection.selectionChanged                 += OnSelectionChanged;
		EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemOnGUI;
	}

	static void OnSelectionChanged()
	{
		if(PrefabStageUtility.GetCurrentPrefabStage()==null) return;
		selectedObject = Selection.activeGameObject;
		UpdateReferences();
	}

	static void UpdateReferences()
	{
		referencedObjects.Clear();
		referencingObjects.Clear();
		if(selectedObject==null) return;

		// Find referenced objects **********
		var components = selectedObject.GetComponents<Component>();
		foreach (var component in components) {
			if(component==null || component.GetType()==typeof(Transform)) continue; // ignore transforms so we don't get lines to root and parent
			var fields = component.GetType().GetFields(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance);
			foreach (var field in fields) {
				if(typeof(Object).IsAssignableFrom(field.FieldType)) {
					var value = field.GetValue(component) as Object;
					AddReferencedObject(value);
				} else if(typeof(IEnumerable<Object>).IsAssignableFrom(field.FieldType)) {
					var enumerable = field.GetValue(component) as IEnumerable<Object>;
					if(enumerable!=null) {
						foreach (var item in enumerable) AddReferencedObject(item);
					}
				} else if(field.FieldType.IsArray && typeof(Object).IsAssignableFrom(field.FieldType.GetElementType())) {
					var array = field.GetValue(component) as Object[];
					if(array!=null) {
						foreach (var item in array) AddReferencedObject(item);
					}
				}
			}

			// Also check properties
			var properties = component.GetType().GetProperties(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance);
			foreach (var property in properties) {
				if(property.CanRead && typeof(Object).IsAssignableFrom(property.PropertyType)) {
					try {
						if(component is Renderer || component is MeshFilter) continue;
						var value = property.GetValue(component,null) as Object;
						AddReferencedObject(value);
					} catch { } // Skip properties that can't be accessed
				}
			}
		}

		// Find referencing objects *********
		GameObject[] allGameObjects;
		if(PrefabStageUtility.GetCurrentPrefabStage()!=null) {
			// In prefab edit mode
			allGameObjects = PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot.GetComponentsInChildren<Transform>().Select(t => t.gameObject).ToArray();
		} else {
			// In normal scene mode
			allGameObjects = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects();
		}
		foreach (var go in allGameObjects) {
			if(go==selectedObject) continue;
			bool isReferencing = false;
			var  comps         = go.GetComponents<Component>();
			foreach (var comp in comps) {
				if(comp==null || comp.GetType()==typeof(Transform)) continue; // ignore transforms so we don't get lines to root and parent
				// Check fields
				var fields = comp.GetType().GetFields(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance);
				foreach (var field in fields) {
					if(typeof(Object).IsAssignableFrom(field.FieldType)) {
						var value = field.GetValue(comp) as Object;
						if(IsReferencingSelectedObject(value)) {
							isReferencing = true;
							break;
						}
					} else if(typeof(IEnumerable<Object>).IsAssignableFrom(field.FieldType)) {
						var enumerable = field.GetValue(comp) as IEnumerable<Object>;
						if(enumerable!=null) {
							foreach (var item in enumerable) {
								if(IsReferencingSelectedObject(item)) {
									isReferencing = true;
									break;
								}
							}
						}
					}
				}

				// Check properties
				var properties = comp.GetType().GetProperties(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance);
				foreach (var property in properties) {
					if(property.CanRead && typeof(Object).IsAssignableFrom(property.PropertyType)) {
						try {
							if(comp is Renderer || comp is MeshFilter) continue;
							var value = property.GetValue(comp,null) as Object;
							if(IsReferencingSelectedObject(value)) {
								isReferencing = true;
								break;
							}
						} catch { } // Skip properties that can't be accessed
					}
				}
				if(isReferencing) break;
			}
			if(isReferencing) { referencingObjects.Add(go); }
		}
	}

	static bool IsReferencingSelectedObject(Object obj)
	{
		if(!obj) return false;
		if(obj==selectedObject) return true;
		if(obj is Component comp && comp.gameObject==selectedObject) return true;
		return false;
	}

	static void AddReferencedObject(Object obj)
	{
		if(obj==null) return;
		GameObject go = null;
		if(obj is GameObject gameObject) { go = gameObject; } else if(obj is Component comp) { go = comp.gameObject; }
		if(go!=null && go!=selectedObject) { referencedObjects.Add(go); }
	}

	static void OnHierarchyWindowItemOnGUI(int instanceID,Rect rect)
	{
		if(PrefabStageUtility.GetCurrentPrefabStage()==null) return;
		if(selectedObject==null) return;
		var obj = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
		if(obj==null) return;
		// if(obj!=selectedObject) return;
		itemRects[instanceID] = rect;
		if(obj==selectedObject) selectedRect = rect;
		if(Event.current.type==EventType.Repaint) DrawCurves();
	}

	static void DrawCurves()
	{
		if(selectedRect==Rect.zero) return;
		Handles.BeginGUI();
		foreach (var go in referencedObjects) { DrawLineToObject(go,Color.blue,false); }
		foreach (var go in referencingObjects) { DrawLineToObject(go,Color.red,true); }
		Handles.EndGUI();
	}

	static void DrawLineToObject(GameObject go,Color color,bool isReferencing)
	{
		int id = go.GetInstanceID();
		if(itemRects.TryGetValue(id,out Rect targetRect)) {
			if(isReferencing) DrawBezier(targetRect,selectedRect,color);
			else DrawBezier(selectedRect,targetRect,color);
		} else {
			// Handle objects outside the visible area
			Rect edgeRect = GetEdgeRect(isReferencing);
			if(isReferencing) DrawBezier(edgeRect,selectedRect,color);
			else DrawBezier(selectedRect,edgeRect,color);
		}
	}

	static Rect GetEdgeRect(bool isReferencing)
	{
		Rect hierarchyRect = GetHierarchyWindowRect();
		Rect edgeRect      = selectedRect;
		if(isReferencing) edgeRect.y = hierarchyRect.yMin;                     // Top edge
		else edgeRect.y              = hierarchyRect.yMax-selectedRect.height; // Bottom edge
		return edgeRect;
	}

	static void DrawBezier(Rect fromRect,Rect toRect,Color color)
	{
		Vector2 startPos     = new Vector2(fromRect.xMin+15,fromRect.y+fromRect.height/2);
		Vector2 endPos       = new Vector2(toRect.xMin,toRect.y+toRect.height/2);
		Vector2 startTangent = startPos+Vector2.right*50;
		Vector2 endTangent   = endPos+Vector2.left*50;
		Handles.color = color;
		Handles.DrawBezier(startPos,endPos,startTangent,endTangent,color,null,1);
	}

	static Rect GetHierarchyWindowRect()
	{
		foreach (var window in Resources.FindObjectsOfTypeAll<EditorWindow>()) {
			if(window.titleContent.text=="Hierarchy") return window.position;
		}
		return new Rect(0,0,Screen.width,Screen.height);
	}
}
1 Like

This is pretty cool. What a nice idea. Simple and useful. Thanks for sharing.

I think Unity was headed in the direction of adding things like this officially with the search extensions package and the Dependency viewer, with some pretty awesome people behind them. Some higher-ups decided remove resources from those projects, and now they may never be completed. Unity is supposed to win over Unreal on usability; it needs its own reference graph.

Glad you like and feel free to improve!

And merry Christmas!