using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using Interactions; using System.Reflection; using System; namespace AppleHills.Editor.InteractionSystem { /// /// Editor utility for managing and debugging interactable objects in the scene. /// Provides scene object locator, inspector editing, and runtime debugging capabilities. /// public class InteractableEditorWindow : EditorWindow { // Tab management private int _selectedTab = 0; private readonly string[] _tabNames = { "Scene", "Debug" }; // Scene interactables tracking private List _sceneInteractables = new List(); private InteractableBase _selectedInteractable; private GameObject _selectedGameObject; // UI state private Vector2 _listScrollPosition; private Vector2 _inspectorScrollPosition; private Vector2 _debugScrollPosition; private string _searchQuery = ""; // Runtime state private bool _isPlaying = false; // Editor for selected interactable private UnityEditor.Editor _cachedEditor; // Available interactable types for adding private static readonly Type[] AvailableInteractableTypes = new Type[] { typeof(OneClickInteraction), typeof(Pickup), typeof(ItemSlot), typeof(SaveableInteractable), typeof(InteractableBase) }; [MenuItem("AppleHills/Interactable Editor")] public static void ShowWindow() { var window = GetWindow("Interactable Editor"); window.minSize = new Vector2(900, 600); window.Show(); } private void OnEnable() { RefreshSceneInteractables(); // Register for scene and selection changes UnityEditor.SceneManagement.EditorSceneManager.sceneOpened += OnSceneOpened; UnityEditor.SceneManagement.EditorSceneManager.sceneClosed += OnSceneClosed; Selection.selectionChanged += OnSelectionChanged; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; EditorApplication.hierarchyChanged += OnHierarchyChanged; } private void OnDisable() { UnityEditor.SceneManagement.EditorSceneManager.sceneOpened -= OnSceneOpened; UnityEditor.SceneManagement.EditorSceneManager.sceneClosed -= OnSceneClosed; Selection.selectionChanged -= OnSelectionChanged; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.hierarchyChanged -= OnHierarchyChanged; // Clean up cached editor if (_cachedEditor != null) { DestroyImmediate(_cachedEditor); _cachedEditor = null; } } private void OnSceneOpened(UnityEngine.SceneManagement.Scene scene, UnityEditor.SceneManagement.OpenSceneMode mode) { RefreshSceneInteractables(); } private void OnSceneClosed(UnityEngine.SceneManagement.Scene scene) { RefreshSceneInteractables(); } private void OnHierarchyChanged() { RefreshSceneInteractables(); } private void OnSelectionChanged() { // Check if selected object has changed if (Selection.activeGameObject != null && Selection.activeGameObject != _selectedGameObject) { var interactable = Selection.activeGameObject.GetComponent(); if (interactable != null) { // GameObject has an interactable - select it SelectInteractable(interactable); } else { // GameObject doesn't have an interactable - track it for add menu _selectedGameObject = Selection.activeGameObject; _selectedInteractable = null; // Clear cached editor if (_cachedEditor != null) { DestroyImmediate(_cachedEditor); _cachedEditor = null; } } Repaint(); } else if (Selection.activeGameObject == null) { // Nothing selected - clear selection _selectedGameObject = null; _selectedInteractable = null; if (_cachedEditor != null) { DestroyImmediate(_cachedEditor); _cachedEditor = null; } Repaint(); } } private void OnPlayModeStateChanged(PlayModeStateChange state) { _isPlaying = EditorApplication.isPlaying; if (_isPlaying) { RefreshSceneInteractables(); } Repaint(); } private void OnGUI() { DrawHeader(); _selectedTab = GUILayout.Toolbar(_selectedTab, _tabNames); EditorGUILayout.Space(); switch (_selectedTab) { case 0: // Scene tab DrawSceneTab(); break; case 1: // Debug tab DrawDebugTab(); break; } } #region Header UI private void DrawHeader() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60))) { RefreshSceneInteractables(); } GUILayout.FlexibleSpace(); // Tab-specific toolbar options if (_selectedTab == 0) // Scene tab { EditorGUILayout.LabelField($"Found: {_sceneInteractables.Count} interactables", EditorStyles.toolbarButton, GUILayout.Width(150)); } else if (_selectedTab == 1) // Debug tab { EditorGUILayout.LabelField(_isPlaying ? "Runtime Active" : "Editor Mode", EditorStyles.toolbarButton, GUILayout.Width(100)); } EditorGUILayout.EndHorizontal(); } #endregion #region Scene Tab private void DrawSceneTab() { EditorGUILayout.BeginHorizontal(); // Left panel - interactable list EditorGUILayout.BeginVertical(GUILayout.Width(300)); DrawInteractableListPanel(); EditorGUILayout.EndVertical(); // Separator EditorGUILayout.Space(5); // Right panel - inspector/editor EditorGUILayout.BeginVertical(); DrawInspectorPanel(); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); } private void DrawInteractableListPanel() { // Search field EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); _searchQuery = EditorGUILayout.TextField(_searchQuery, EditorStyles.toolbarSearchField); if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(20)) && !string.IsNullOrEmpty(_searchQuery)) { _searchQuery = ""; GUI.FocusControl(null); } EditorGUILayout.EndHorizontal(); _listScrollPosition = EditorGUILayout.BeginScrollView(_listScrollPosition); // Filter interactables by search query var filteredInteractables = string.IsNullOrEmpty(_searchQuery) ? _sceneInteractables : _sceneInteractables.Where(i => i != null && i.gameObject.name.ToLower().Contains(_searchQuery.ToLower())).ToList(); if (filteredInteractables.Count == 0) { EditorGUILayout.HelpBox("No interactables found in scene", MessageType.Info); } else { foreach (var interactable in filteredInteractables) { if (interactable == null) continue; if (DrawInteractableListItem(interactable)) { SelectInteractable(interactable); } } } EditorGUILayout.EndScrollView(); } private bool DrawInteractableListItem(InteractableBase interactable) { bool isSelected = interactable == _selectedInteractable; Color originalColor = GUI.backgroundColor; if (isSelected) GUI.backgroundColor = new Color(0.3f, 0.5f, 0.8f); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUI.backgroundColor = originalColor; EditorGUILayout.BeginHorizontal(); // Interactable info EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField(interactable.gameObject.name, EditorStyles.boldLabel); EditorGUILayout.LabelField(interactable.GetType().Name, EditorStyles.miniLabel); // Show additional info for specific types if (interactable is Pickup pickup && pickup.itemData != null) { EditorGUILayout.LabelField($"Item: {pickup.itemData.itemName}", EditorStyles.miniLabel); } else if (interactable is ItemSlot slot && slot.itemData != null) { EditorGUILayout.LabelField($"Slot: {slot.itemData.itemName}", EditorStyles.miniLabel); } EditorGUILayout.EndVertical(); GUILayout.FlexibleSpace(); // Ping button if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(interactable.gameObject); Selection.activeGameObject = interactable.gameObject; } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); Rect itemRect = GUILayoutUtility.GetLastRect(); bool wasClicked = Event.current.type == EventType.MouseDown && Event.current.button == 0 && itemRect.Contains(Event.current.mousePosition); if (wasClicked) { Event.current.Use(); } return wasClicked; } private void DrawInspectorPanel() { _inspectorScrollPosition = EditorGUILayout.BeginScrollView(_inspectorScrollPosition); if (_selectedInteractable == null && _selectedGameObject == null) { EditorGUILayout.HelpBox("Select an interactable from the list or in the scene hierarchy", MessageType.Info); } else if (_selectedGameObject != null && _selectedInteractable == null) { // Selected object doesn't have an interactable - show add menu DrawAddInteractableMenu(); } else if (_selectedInteractable != null) { // Draw custom inspector for the selected interactable DrawInteractableInspector(); } EditorGUILayout.EndScrollView(); } private void DrawAddInteractableMenu() { EditorGUILayout.HelpBox($"GameObject '{_selectedGameObject.name}' doesn't have an Interactable component", MessageType.Info); EditorGUILayout.Space(); EditorGUILayout.LabelField("Add Interactable Component:", EditorStyles.boldLabel); foreach (var interactableType in AvailableInteractableTypes) { if (GUILayout.Button($"Add {interactableType.Name}", GUILayout.Height(30))) { Undo.RecordObject(_selectedGameObject, $"Add {interactableType.Name}"); var component = _selectedGameObject.AddComponent(interactableType) as InteractableBase; EditorUtility.SetDirty(_selectedGameObject); SelectInteractable(component); RefreshSceneInteractables(); } } } private void DrawInteractableInspector() { // Header EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Editing:", EditorStyles.boldLabel, GUILayout.Width(60)); EditorGUILayout.LabelField(_selectedInteractable.gameObject.name, EditorStyles.boldLabel); GUILayout.FlexibleSpace(); if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(_selectedInteractable.gameObject); Selection.activeGameObject = _selectedInteractable.gameObject; } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // Draw default inspector using Editor if (_cachedEditor == null || _cachedEditor.target != _selectedInteractable) { if (_cachedEditor != null) { DestroyImmediate(_cachedEditor); } _cachedEditor = UnityEditor.Editor.CreateEditor(_selectedInteractable); } if (_cachedEditor != null) { EditorGUI.BeginChangeCheck(); _cachedEditor.OnInspectorGUI(); if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(_selectedInteractable); } } EditorGUILayout.Space(); // Additional info section DrawAdditionalInfo(); } private void DrawAdditionalInfo() { EditorGUILayout.LabelField("Additional Information", EditorStyles.boldLabel); EditorGUILayout.BeginVertical(EditorStyles.helpBox); // Show component information EditorGUILayout.LabelField("Type:", _selectedInteractable.GetType().Name); // Show attached actions var actions = _selectedInteractable.GetComponents(); if (actions.Length > 0) { EditorGUILayout.LabelField($"Actions: {actions.Length}"); EditorGUI.indentLevel++; foreach (var action in actions) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(action.GetType().Name, EditorStyles.miniLabel); if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(action); } EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel--; } // Show specific type info if (_selectedInteractable is Pickup pickup) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Pickup Info:", EditorStyles.boldLabel); if (pickup.itemData != null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Item Data:", GUILayout.Width(100)); EditorGUILayout.ObjectField(pickup.itemData, typeof(PickupItemData), false); if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(pickup.itemData); } EditorGUILayout.EndHorizontal(); } } else if (_selectedInteractable is ItemSlot slot) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Slot Info:", EditorStyles.boldLabel); if (slot.itemData != null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Slot Data:", GUILayout.Width(100)); EditorGUILayout.ObjectField(slot.itemData, typeof(PickupItemData), false); if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(slot.itemData); } EditorGUILayout.EndHorizontal(); } if (_isPlaying) { var slottedObject = slot.GetSlottedObject(); if (slottedObject != null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Slotted Object:", GUILayout.Width(100)); EditorGUILayout.ObjectField(slottedObject, typeof(GameObject), true); if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(slottedObject); } EditorGUILayout.EndHorizontal(); } } } EditorGUILayout.EndVertical(); } #endregion #region Debug Tab private void DrawDebugTab() { if (!_isPlaying) { EditorGUILayout.HelpBox("Enter Play Mode to debug interactables at runtime", MessageType.Info); return; } _debugScrollPosition = EditorGUILayout.BeginScrollView(_debugScrollPosition); EditorGUILayout.LabelField("Scene Interactables", EditorStyles.boldLabel); EditorGUILayout.HelpBox("Test interactions and trigger individual events", MessageType.Info); EditorGUILayout.Space(); foreach (var interactable in _sceneInteractables) { if (interactable == null) continue; DrawDebugInteractableItem(interactable); } EditorGUILayout.EndScrollView(); } private void DrawDebugInteractableItem(InteractableBase interactable) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); // Header EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(interactable.gameObject.name, EditorStyles.boldLabel); EditorGUILayout.LabelField($"({interactable.GetType().Name})", EditorStyles.miniLabel); GUILayout.FlexibleSpace(); if (GUILayout.Button("Ping", GUILayout.Width(50))) { EditorGUIUtility.PingObject(interactable.gameObject); Selection.activeGameObject = interactable.gameObject; } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // Interaction buttons EditorGUILayout.LabelField("Trigger Interaction:", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Full Interaction", GUILayout.Height(25))) { TriggerFullInteraction(interactable); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // Event buttons EditorGUILayout.LabelField("Trigger Individual Events:", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Started")) { TriggerEvent(interactable, "OnInteractionStarted"); TriggerUnityEvent(interactable, "interactionStarted"); } if (GUILayout.Button("Arrived")) { TriggerEvent(interactable, "OnInteractingCharacterArrived"); TriggerUnityEvent(interactable, "characterArrived"); } if (GUILayout.Button("Do Interaction")) { TriggerEvent(interactable, "DoInteraction"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Complete (Success)")) { TriggerEventWithParam(interactable, "OnInteractionFinished", true); TriggerUnityEventWithParam(interactable, "interactionComplete", true); } if (GUILayout.Button("Complete (Fail)")) { TriggerEventWithParam(interactable, "OnInteractionFinished", false); TriggerUnityEventWithParam(interactable, "interactionComplete", false); } if (GUILayout.Button("Interrupted")) { TriggerUnityEvent(interactable, "interactionInterrupted"); } EditorGUILayout.EndHorizontal(); // Show registered actions var actions = interactable.GetComponents(); if (actions.Length > 0) { EditorGUILayout.Space(); EditorGUILayout.LabelField($"Registered Actions ({actions.Length}):", EditorStyles.boldLabel); foreach (var action in actions) { EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); EditorGUILayout.LabelField(action.GetType().Name); if (action.respondToEvents != null && action.respondToEvents.Count > 0) { string events = string.Join(", ", action.respondToEvents); EditorGUILayout.LabelField($"Events: {events}", EditorStyles.miniLabel); } EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } #endregion #region Data Management private void RefreshSceneInteractables() { _sceneInteractables.Clear(); // Find all interactables in the scene var allInteractables = FindObjectsByType(FindObjectsSortMode.None); _sceneInteractables.AddRange(allInteractables); // Sort by name for easier browsing _sceneInteractables.Sort((a, b) => { if (a == null || b == null) return 0; return string.Compare(a.gameObject.name, b.gameObject.name, StringComparison.Ordinal); }); } private void SelectInteractable(InteractableBase interactable) { _selectedInteractable = interactable; _selectedGameObject = interactable?.gameObject; // Clear cached editor to force recreation if (_cachedEditor != null) { DestroyImmediate(_cachedEditor); _cachedEditor = null; } } #endregion #region Debug Helpers private void TriggerFullInteraction(InteractableBase interactable) { if (!_isPlaying || interactable == null) return; // Simulate a tap on the interactable Vector3 worldPos = interactable.transform.position; interactable.OnTap(new Vector2(worldPos.x, worldPos.y)); Debug.Log($"[Interactable Editor] Triggered full interaction on {interactable.gameObject.name}"); } private void TriggerEvent(InteractableBase interactable, string methodName) { if (!_isPlaying || interactable == null) return; Type type = interactable.GetType(); MethodInfo method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method != null) { try { method.Invoke(interactable, null); Debug.Log($"[Interactable Editor] Invoked {methodName} on {interactable.gameObject.name}"); } catch (Exception e) { Debug.LogError($"[Interactable Editor] Error invoking {methodName}: {e.Message}"); } } else { Debug.LogWarning($"[Interactable Editor] Method {methodName} not found on {type.Name}"); } } private void TriggerEventWithParam(InteractableBase interactable, string methodName, object param) { if (!_isPlaying || interactable == null) return; Type type = interactable.GetType(); MethodInfo method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method != null) { try { method.Invoke(interactable, new object[] { param }); Debug.Log($"[Interactable Editor] Invoked {methodName}({param}) on {interactable.gameObject.name}"); } catch (Exception e) { Debug.LogError($"[Interactable Editor] Error invoking {methodName}: {e.Message}"); } } else { Debug.LogWarning($"[Interactable Editor] Method {methodName} not found on {type.Name}"); } } private void TriggerUnityEvent(InteractableBase interactable, string fieldName) { if (!_isPlaying || interactable == null) return; Type type = interactable.GetType(); FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.GetValue(interactable) is UnityEngine.Events.UnityEventBase unityEvent) { // Use reflection to invoke the protected Invoke method MethodInfo invokeMethod = unityEvent.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (invokeMethod != null) { invokeMethod.Invoke(unityEvent, null); Debug.Log($"[Interactable Editor] Invoked UnityEvent {fieldName} on {interactable.gameObject.name}"); } } } private void TriggerUnityEventWithParam(InteractableBase interactable, string fieldName, bool param) { if (!_isPlaying || interactable == null) return; Type type = interactable.GetType(); FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.GetValue(interactable) is UnityEngine.Events.UnityEvent unityEvent) { unityEvent.Invoke(param); Debug.Log($"[Interactable Editor] Invoked UnityEvent {fieldName}({param}) on {interactable.gameObject.name}"); } } #endregion } }