using UnityEngine; using UnityEditor; using UnityEngine.Events; using System.Collections.Generic; using System.Reflection; using System.Linq; public class UnityEventScanner : EditorWindow { private Vector2 scroll; private List foundEvents = new List(); private string searchQuery = ""; // Keep persistent bulk values private GameObject bulkTarget; private int bulkComponentIndex = 0; [MenuItem("Tools/Unity Event Scanner")] public static void ShowWindow() { GetWindow("Unity Event Scanner"); } private void OnGUI() { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Scan All Scenes for UnityEvents")) { ScanAllSceneObjects(); } if (GUILayout.Button("Scan Selected GameObjects")) { ScanSelectedObjectsFromUI(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.LabelField("Search by GameObject Name:"); searchQuery = EditorGUILayout.TextField(searchQuery); EditorGUILayout.Space(); GUIStyle fieldLabelStyle = new GUIStyle(EditorStyles.label) { fontStyle = FontStyle.Bold, normal = { textColor = Color.cyan } }; scroll = EditorGUILayout.BeginScrollView(scroll); foreach (var evt in foundEvents) { if (!string.IsNullOrEmpty(searchQuery) && !evt.gameObject.name.ToLower().Contains(searchQuery.ToLower())) continue; evt.selectedForBulk = EditorGUILayout.ToggleLeft("✔ Select for Bulk Add", evt.selectedForBulk); EditorGUILayout.LabelField("GameObject:", evt.gameObject.name); EditorGUILayout.LabelField("Component:", evt.component.GetType().Name); EditorGUILayout.LabelField("Field:", evt.fieldName, fieldLabelStyle); evt.targetGameObject = (GameObject)EditorGUILayout.ObjectField("Target GameObject", evt.targetGameObject, typeof(GameObject), true); EditorGUILayout.Space(); } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); EditorGUILayout.LabelField("⚡ Bulk Add Listener", EditorStyles.boldLabel); bulkTarget = (GameObject)EditorGUILayout.ObjectField("Target GameObject", bulkTarget, typeof(GameObject), true); if (bulkTarget != null) { var comps = bulkTarget.GetComponents(); if (comps.Length == 0) { EditorGUILayout.HelpBox("No MonoBehaviour components found on this GameObject.", MessageType.Warning); return; } var compNames = comps.Select(c => c.GetType().Name).ToArray(); bulkComponentIndex = EditorGUILayout.Popup("Component", bulkComponentIndex, compNames); var bulkComponent = comps[bulkComponentIndex]; var methodNames = GetValidMethodNames(bulkComponent, typeof(UnityEvent), out System.Type paramType).ToArray(); if (methodNames.Length == 0) { EditorGUILayout.HelpBox("No valid methods found on component.", MessageType.Warning); return; } int methodIndex = EditorGUILayout.Popup("Method", 0, methodNames); object paramValue = null; if (paramType == typeof(string)) paramValue = EditorGUILayout.TextField("Value", ""); else if (paramType == typeof(int)) paramValue = EditorGUILayout.IntField("Value", 0); else if (paramType == typeof(float)) paramValue = EditorGUILayout.FloatField("Value", 0f); else if (paramType == typeof(bool)) paramValue = EditorGUILayout.Toggle("Value", false); if (GUILayout.Button("👉 Apply to Selected UnityEvents")) { foreach (var evt in foundEvents.Where(e => e.selectedForBulk)) { evt.targetGameObject = bulkTarget; evt.targetComponents = new List { (MonoBehaviour)bulkComponent }; evt.componentNames = new List { bulkComponent.GetType().Name }; evt.selectedComponentIndex = 0; evt.availableMethods = methodNames.ToList(); evt.selectedMethodIndex = methodIndex; evt.methodParameterType = paramType; evt.parameterValue = paramValue; } AddListenersInBulk(foundEvents.Where(e => e.selectedForBulk).ToList(), bulkComponent, methodNames[methodIndex]); Debug.Log("✅ Bulk listener added to selected UnityEvents."); } } } private void ScanAllSceneObjects() { foundEvents.Clear(); var allRoots = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().GetRootGameObjects(); foreach (var root in allRoots) { var components = root.GetComponentsInChildren(true); foreach (var component in components) { if (component == null) continue; var fields = component.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (var field in fields) { var fieldType = field.FieldType; if (typeof(UnityEventBase).IsAssignableFrom(fieldType)) { foundEvents.Add(new UnityEventInfo { gameObject = component.gameObject, component = component, fieldName = field.Name, fieldType = fieldType }); } else if (fieldType.IsClass && fieldType != typeof(string)) { object nestedObject = field.GetValue(component); if (nestedObject == null) continue; var nestedFields = fieldType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (var nestedField in nestedFields) { if (typeof(UnityEventBase).IsAssignableFrom(nestedField.FieldType)) { foundEvents.Add(new UnityEventInfo { gameObject = component.gameObject, component = component, fieldName = field.Name + "." + nestedField.Name, fieldType = nestedField.FieldType }); } } } } } } } private void ScanSelectedObjectsFromUI() { var selectedObjects = Selection.gameObjects; if (selectedObjects == null || selectedObjects.Length == 0) { Debug.LogWarning("No GameObjects selected."); return; } foundEvents.Clear(); foreach (var root in selectedObjects) { var components = root.GetComponentsInChildren(true); foreach (var component in components) { if (component == null) continue; var fields = component.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (var field in fields) { var fieldType = field.FieldType; if (typeof(UnityEventBase).IsAssignableFrom(fieldType)) { foundEvents.Add(new UnityEventInfo { gameObject = component.gameObject, component = component, fieldName = field.Name, fieldType = fieldType }); } else if (fieldType.IsClass && fieldType != typeof(string)) { object nestedObject = field.GetValue(component); if (nestedObject == null) continue; var nestedFields = fieldType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (var nestedField in nestedFields) { if (typeof(UnityEventBase).IsAssignableFrom(nestedField.FieldType)) { foundEvents.Add(new UnityEventInfo { gameObject = component.gameObject, component = component, fieldName = field.Name + "." + nestedField.Name, fieldType = nestedField.FieldType }); } } } } } } } private List GetValidMethodNames(Component component, System.Type unityEventType, out System.Type parameterType) { List methodNames = new(); parameterType = null; if (unityEventType.IsGenericType) { var args = unityEventType.GetGenericArguments(); if (args.Length == 1) parameterType = args[0]; } var methods = component.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (var method in methods) { var parameters = method.GetParameters(); if (parameterType == null && parameters.Length == 0 && method.ReturnType == typeof(void)) methodNames.Add(method.Name); else if (parameterType != null && parameters.Length == 1 && parameters[0].ParameterType == parameterType && method.ReturnType == typeof(void)) methodNames.Add(method.Name); } return methodNames; } private void AddListenersInBulk(List selectedEvents, Component bulkComponent, string method) { Undo.RecordObjects(selectedEvents.Select(e => e.component).ToArray(), "Bulk Add UnityEvent Listeners"); foreach (var evt in selectedEvents) { SerializedObject so = new SerializedObject(evt.component); SerializedProperty eventProp = FindNestedProperty(so, evt.fieldName); if (eventProp == null) { Debug.LogWarning($"Cannot find UnityEvent property: {evt.fieldName}"); continue; } SerializedProperty callsArray = eventProp.FindPropertyRelative("m_PersistentCalls.m_Calls"); callsArray.arraySize++; so.ApplyModifiedProperties(); so.Update(); int newIndex = callsArray.arraySize - 1; SerializedProperty newCall = callsArray.GetArrayElementAtIndex(newIndex); SerializedProperty target = newCall.FindPropertyRelative("m_Target"); SerializedProperty methodName = newCall.FindPropertyRelative("m_MethodName"); SerializedProperty mode = newCall.FindPropertyRelative("m_Mode"); SerializedProperty arguments = newCall.FindPropertyRelative("m_Arguments"); SerializedProperty callState = newCall.FindPropertyRelative("m_CallState"); target.objectReferenceValue = bulkComponent; methodName.stringValue = method; callState.enumValueIndex = 2; if (evt.methodParameterType == null) { mode.enumValueIndex = 1; } else if (evt.methodParameterType == typeof(string)) { mode.enumValueIndex = 2; arguments.FindPropertyRelative("m_ObjectArgument").stringValue = evt.parameterValue as string; arguments.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = typeof(string).AssemblyQualifiedName; } else if (evt.methodParameterType == typeof(int)) { mode.enumValueIndex = 3; arguments.FindPropertyRelative("m_IntArgument").intValue = (int)evt.parameterValue; } else if (evt.methodParameterType == typeof(float)) { mode.enumValueIndex = 4; arguments.FindPropertyRelative("m_FloatArgument").floatValue = (float)evt.parameterValue; } else if (evt.methodParameterType == typeof(bool)) { mode.enumValueIndex = 5; arguments.FindPropertyRelative("m_BoolArgument").boolValue = (bool)evt.parameterValue; } so.ApplyModifiedProperties(); EditorUtility.SetDirty(evt.component); } } private SerializedProperty FindNestedProperty(SerializedObject obj, string path) { string[] parts = path.Split('.'); SerializedProperty current = obj.FindProperty(parts[0]); for (int i = 1; i < parts.Length; i++) { if (current == null) return null; current = current.FindPropertyRelative(parts[i]); } return current; } private class UnityEventInfo { public GameObject gameObject; public Component component; public string fieldName; public System.Type fieldType; public GameObject targetGameObject; public List targetComponents; public List componentNames; public int selectedComponentIndex = 0; public int selectedMethodIndex = 0; public List availableMethods; public System.Type methodParameterType; public object parameterValue; public bool selectedForBulk = false; } }