using UnityEditor; using UnityEngine; using System; using System.Collections.Generic; using System.Linq; using UnityEditor.SceneManagement; using UnityEngine.SceneManagement; using UnityEditor.IMGUI.Controls; namespace Editor.Tools { public class ComponentSearchReplaceWindow : EditorWindow { private Type selectedSearchType; private Type selectedReplaceType; private List foundComponents = new List(); private Vector2 scrollPos; private int selectedComponentIndex = -1; private string searchTypeName = "Select a Component..."; private string replaceTypeName = "Select a Component..."; private List allMonoBehaviourTypes = new List(); private bool includeDerivedTypes = true; [MenuItem("Tools/Component Search & Replace")] public static void ShowWindow() { var window = GetWindow("Component Search & Replace"); window.minSize = new Vector2(700, 500); window.maxSize = new Vector2(1400, 1000); window.position = new Rect(120, 120, 800, 600); } private void OnEnable() { CacheMonoBehaviourTypes(); EditorSceneManager.sceneOpened += (scene, mode) => ClearSearch(); SceneManager.activeSceneChanged += (oldScene, newScene) => ClearSearch(); } private void OnDisable() { EditorSceneManager.sceneOpened -= (scene, mode) => ClearSearch(); SceneManager.activeSceneChanged -= (oldScene, newScene) => ClearSearch(); } private void CacheMonoBehaviourTypes() { allMonoBehaviourTypes.Clear(); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { try { var types = assembly.GetTypes() .Where(t => (t.IsSubclassOf(typeof(MonoBehaviour)) || t.IsSubclassOf(typeof(Component))) && !t.IsAbstract && t != typeof(Transform)) // Exclude Transform as it can't be removed .OrderBy(t => t.Name); allMonoBehaviourTypes.AddRange(types); } catch (Exception) { // Some assemblies may throw exceptions when getting types continue; } } } private void ClearSearch() { foundComponents.Clear(); selectedComponentIndex = -1; Repaint(); } private void OnGUI() { GUILayout.Space(10); // Search Section EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("Search for Component", EditorStyles.boldLabel); GUILayout.Space(5); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Component Type:", GUILayout.Width(120)); if (GUILayout.Button(searchTypeName, EditorStyles.popup, GUILayout.ExpandWidth(true))) { var dropdown = new ComponentTypeDropdown( new AdvancedDropdownState(), (type) => { selectedSearchType = type; searchTypeName = type != null ? type.Name : "Select a Component..."; ClearSearch(); Repaint(); }); dropdown.Show(GUILayoutUtility.GetLastRect()); } EditorGUILayout.EndHorizontal(); GUILayout.Space(5); // Include Derived Types checkbox includeDerivedTypes = EditorGUILayout.Toggle( new GUIContent("Include Derived Types", "When enabled, searches for the selected type and all types that inherit from it. " + "When disabled, searches only for the exact type."), includeDerivedTypes); GUILayout.Space(5); EditorGUI.BeginDisabledGroup(selectedSearchType == null); if (GUILayout.Button("Search Scene", GUILayout.Height(30))) { SearchForComponents(); } EditorGUI.EndDisabledGroup(); EditorGUILayout.EndVertical(); GUILayout.Space(10); // Results Section EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField($"Search Results ({foundComponents.Count} found)", EditorStyles.boldLabel); GUILayout.Space(5); scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.ExpandHeight(true)); if (foundComponents.Count == 0) { EditorGUILayout.HelpBox("No components found. Select a component type and click 'Search Scene'.", MessageType.Info); } else { for (int i = 0; i < foundComponents.Count; i++) { var info = foundComponents[i]; if (info.component == null) continue; bool isSelected = selectedComponentIndex == i; Color originalColor = GUI.backgroundColor; if (isSelected) { GUI.backgroundColor = new Color(0.3f, 0.6f, 1f, 0.5f); } EditorGUILayout.BeginVertical("box"); GUI.backgroundColor = originalColor; EditorGUILayout.BeginHorizontal(); // Selection toggle bool newSelected = GUILayout.Toggle(isSelected, "", GUILayout.Width(20)); if (newSelected != isSelected) { selectedComponentIndex = newSelected ? i : -1; } EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField($"GameObject: {info.gameObject.name}", EditorStyles.boldLabel); EditorGUILayout.LabelField($"Path: {info.hierarchyPath}", EditorStyles.miniLabel); EditorGUILayout.LabelField($"Component: {info.component.GetType().Name}", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); if (GUILayout.Button("Ping", GUILayout.Width(80))) { Selection.activeObject = info.gameObject; EditorGUIUtility.PingObject(info.gameObject); SceneView.FrameLastActiveSceneView(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); GUILayout.Space(2); } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); GUILayout.Space(10); // Replace Section EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("Replace Component", EditorStyles.boldLabel); GUILayout.Space(5); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Replace With:", GUILayout.Width(120)); if (GUILayout.Button(replaceTypeName, EditorStyles.popup, GUILayout.ExpandWidth(true))) { var dropdown = new ComponentTypeDropdown( new AdvancedDropdownState(), (type) => { selectedReplaceType = type; replaceTypeName = type != null ? type.Name : "Select a Component..."; Repaint(); }); dropdown.Show(GUILayoutUtility.GetLastRect()); } EditorGUILayout.EndHorizontal(); GUILayout.Space(5); bool canReplace = selectedComponentIndex >= 0 && selectedComponentIndex < foundComponents.Count && selectedReplaceType != null; EditorGUI.BeginDisabledGroup(!canReplace); if (!canReplace && selectedComponentIndex >= 0 && selectedReplaceType == null) { EditorGUILayout.HelpBox("Select a component type to replace with.", MessageType.Warning); } else if (!canReplace && selectedReplaceType != null) { EditorGUILayout.HelpBox("Select an object from the search results above.", MessageType.Warning); } if (GUILayout.Button("Replace Selected Component", GUILayout.Height(30))) { ReplaceComponent(); } EditorGUI.EndDisabledGroup(); EditorGUILayout.EndVertical(); GUILayout.Space(5); } private void SearchForComponents() { if (selectedSearchType == null) { Debug.LogWarning("No component type selected for search."); return; } foundComponents.Clear(); selectedComponentIndex = -1; var allObjects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var go in allObjects) { Component component = null; if (includeDerivedTypes) { // Search for the type and all derived types component = go.GetComponent(selectedSearchType); } else { // Search for exact type only var components = go.GetComponents(); component = components.FirstOrDefault(c => c != null && c.GetType() == selectedSearchType); } if (component != null) { foundComponents.Add(new ComponentInfo { gameObject = go, component = component, hierarchyPath = GetHierarchyPath(go) }); } } foundComponents = foundComponents.OrderBy(c => c.hierarchyPath).ToList(); string searchMode = includeDerivedTypes ? "including derived types" : "exact type only"; Debug.Log($"Found {foundComponents.Count} objects with component type '{selectedSearchType.Name}' ({searchMode})"); Repaint(); } private void ReplaceComponent() { if (selectedComponentIndex < 0 || selectedComponentIndex >= foundComponents.Count) { Debug.LogError("No component selected to replace."); return; } if (selectedReplaceType == null) { Debug.LogError("No replacement component type selected."); return; } var info = foundComponents[selectedComponentIndex]; var go = info.gameObject; var oldComponent = info.component; if (go == null || oldComponent == null) { Debug.LogError("Selected component or GameObject is null."); foundComponents.RemoveAt(selectedComponentIndex); selectedComponentIndex = -1; Repaint(); return; } // Confirm the replacement bool confirmed = EditorUtility.DisplayDialog( "Replace Component", $"Are you sure you want to replace '{selectedSearchType.Name}' with '{selectedReplaceType.Name}' on GameObject '{go.name}'?\n\n" + "This action can be undone with Ctrl+Z.", "Replace", "Cancel" ); if (!confirmed) { return; } // Record undo Undo.RegisterCompleteObjectUndo(go, "Replace Component"); // Copy serialized data if possible (optional enhancement) // This attempts to preserve common fields between components var serializedOldComponent = new SerializedObject(oldComponent); // Remove old component Undo.DestroyObjectImmediate(oldComponent); // Add new component var newComponent = Undo.AddComponent(go, selectedReplaceType); // Try to copy matching serialized properties var serializedNewComponent = new SerializedObject(newComponent); CopyMatchingSerializedProperties(serializedOldComponent, serializedNewComponent); serializedNewComponent.ApplyModifiedProperties(); // Mark scene as dirty EditorSceneManager.MarkSceneDirty(go.scene); Debug.Log($"Replaced '{selectedSearchType.Name}' with '{selectedReplaceType.Name}' on '{go.name}'"); // Update the found components list foundComponents[selectedComponentIndex] = new ComponentInfo { gameObject = go, component = newComponent, hierarchyPath = info.hierarchyPath }; // Update search type if needed if (selectedSearchType != selectedReplaceType) { foundComponents.RemoveAt(selectedComponentIndex); selectedComponentIndex = -1; } Repaint(); } private void CopyMatchingSerializedProperties(SerializedObject source, SerializedObject destination) { SerializedProperty iterator = source.GetIterator(); bool enterChildren = true; while (iterator.NextVisible(enterChildren)) { enterChildren = false; // Skip script reference if (iterator.propertyPath == "m_Script") continue; SerializedProperty destProp = destination.FindProperty(iterator.propertyPath); if (destProp != null && destProp.propertyType == iterator.propertyType) { try { destination.CopyFromSerializedProperty(iterator); } catch { // Some properties may not be copyable } } } } private string GetHierarchyPath(GameObject go) { string path = go.name; Transform t = go.transform.parent; while (t != null) { path = t.name + "/" + path; t = t.parent; } return path; } private class ComponentInfo { public GameObject gameObject; public Component component; public string hierarchyPath; } } // Advanced Dropdown for Component Type Selection (including Unity built-in components) public class ComponentTypeDropdown : AdvancedDropdown { private Action onTypeSelected; private static List cachedTypes; public ComponentTypeDropdown(AdvancedDropdownState state, Action onTypeSelected) : base(state) { this.onTypeSelected = onTypeSelected; minimumSize = new Vector2(300, 400); if (cachedTypes == null) { CacheTypes(); } } private static void CacheTypes() { cachedTypes = new List(); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { try { var types = assembly.GetTypes() .Where(t => (t.IsSubclassOf(typeof(MonoBehaviour)) || t.IsSubclassOf(typeof(Component))) && !t.IsAbstract && t != typeof(Transform)) // Exclude Transform as it can't be removed .OrderBy(t => t.Name); cachedTypes.AddRange(types); } catch (Exception) { // Some assemblies may throw exceptions when getting types } } } protected override AdvancedDropdownItem BuildRoot() { var root = new AdvancedDropdownItem("Component Types"); // Add "None" option var noneItem = new TypeDropdownItem("None", null); root.AddChild(noneItem); root.AddSeparator(); // Separate Unity built-in components from custom ones var unityComponents = cachedTypes.Where(t => t.Namespace != null && t.Namespace.StartsWith("UnityEngine")).ToList(); var customComponents = cachedTypes.Except(unityComponents).ToList(); // Add Unity Components section if (unityComponents.Any()) { var unitySection = new AdvancedDropdownItem("Unity Components"); // Group Unity components by category (second part of namespace) var unityGrouped = unityComponents.GroupBy(t => { if (t.Namespace == "UnityEngine") return "Core"; var parts = t.Namespace.Split('.'); return parts.Length > 1 ? parts[1] : "Other"; }).OrderBy(g => g.Key); foreach (var group in unityGrouped) { if (group.Count() <= 5) { // Add directly if few items foreach (var type in group.OrderBy(t => t.Name)) { var typeItem = new TypeDropdownItem(type.Name, type); unitySection.AddChild(typeItem); } } else { // Create subcategory var categoryItem = new AdvancedDropdownItem(group.Key); foreach (var type in group.OrderBy(t => t.Name)) { var typeItem = new TypeDropdownItem(type.Name, type); categoryItem.AddChild(typeItem); } unitySection.AddChild(categoryItem); } } root.AddChild(unitySection); } // Add Custom Components section if (customComponents.Any()) { root.AddSeparator(); // Group by namespace var groupedTypes = customComponents.GroupBy(t => string.IsNullOrEmpty(t.Namespace) ? "(No Namespace)" : t.Namespace); foreach (var group in groupedTypes.OrderBy(g => g.Key)) { // For small namespaces or no namespace, add directly to root if (group.Count() <= 3 || group.Key == "(No Namespace)") { foreach (var type in group) { var typeItem = new TypeDropdownItem( group.Key == "(No Namespace)" ? type.Name : $"{group.Key}.{type.Name}", type); root.AddChild(typeItem); } } else { // Create namespace folder var namespaceItem = new AdvancedDropdownItem(group.Key); foreach (var type in group) { var typeItem = new TypeDropdownItem(type.Name, type); namespaceItem.AddChild(typeItem); } root.AddChild(namespaceItem); } } } return root; } protected override void ItemSelected(AdvancedDropdownItem item) { var typeItem = item as TypeDropdownItem; if (typeItem != null) { onTypeSelected?.Invoke(typeItem.Type); } } private class TypeDropdownItem : AdvancedDropdownItem { public Type Type { get; private set; } public TypeDropdownItem(string name, Type type) : base(name) { Type = type; } } } }