Refactoring of the interaction system and preliminary integration of save/load functionality across the game. (#44)
### Interactables Architecture Refactor - Converted composition to inheritance, moved from component-based to class-based interactables. No more requirement for chain of "Interactable -> Item" etc. - Created `InteractableBase` abstract base class with common functionality that replaces the old component - Specialized child classes: `Pickup`, `ItemSlot`, `LevelSwitch`, `MinigameSwitch`, `CombinationItem`, `OneClickInteraction` are now children classes - Light updates to the interactable inspector, moved some things arround, added collapsible inspector sections in the UI for better editor experience ### State Machine Integration - Custom `AppleMachine` inheritong from Pixelplacement's StateMachine which implements our own interface for saving, easy place for future improvements - Replaced all previous StateMachines by `AppleMachine` - Custom `AppleState` extends from default `State`. Added serialization, split state logic into "EnterState", "RestoreState", "ExitState" allowing for separate logic when triggering in-game vs loading game - Restores directly to target state without triggering transitional logic - Migration tool converts existing instances ### Prefab Organization - Saved changes from scenes into prefabs - Cleaned up duplicated components, confusing prefabs hierarchies - Created prefab variants where possible - Consolidated Environment prefabs and moved them out of Placeholders subfolder into main Environment folder - Organized item prefabs from PrefabsPLACEHOLDER into proper Items folder - Updated prefab references - All scene references updated to new locations - Removed placeholder files from Characters, Levels, UI, and Minigames folders ### Scene Updates - Quarry scene with major updates - Saved multiple working versions (Quarry, Quarry_Fixed, Quarry_OLD) - Added proper lighting data - Updated all interactable components to new architecture ### Minor editor tools - New tool for testing cards from an editor window (no in-scene object required) - Updated Interactable Inspector - New debug option to opt in-and-out of the save/load system - Tooling for easier migration Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #44
This commit is contained in:
@@ -6,7 +6,8 @@
|
||||
"GUID:69448af7b92c7f342b298e06a37122aa",
|
||||
"GUID:9e24947de15b9834991c9d8411ea37cf",
|
||||
"GUID:70ef9a24f4cfc4aec911c1414e3f90ad",
|
||||
"GUID:d1e08c06f8f9473888c892637c83c913"
|
||||
"GUID:d1e08c06f8f9473888c892637c83c913",
|
||||
"GUID:db4a9769b2b9c5a4788bcd189eea1f0b"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
||||
369
Assets/Editor/CardSystem/CardSystemTesterWindow.cs
Normal file
369
Assets/Editor/CardSystem/CardSystemTesterWindow.cs
Normal file
@@ -0,0 +1,369 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using AppleHills.Data.CardSystem;
|
||||
using Data.CardSystem;
|
||||
using Core;
|
||||
using UI.CardSystem;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Editor.CardSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window for testing the Card System in play mode.
|
||||
/// Provides buttons to test core functionalities like adding booster packs, opening packs, and generating cards.
|
||||
/// </summary>
|
||||
public class CardSystemTesterWindow : EditorWindow
|
||||
{
|
||||
// Test Settings
|
||||
private int boosterPacksToAdd = 3;
|
||||
private int cardsToGenerate = 10;
|
||||
private bool autoOpenPacksWhenAdded = false;
|
||||
|
||||
// Debug Info
|
||||
private int currentBoosterCount;
|
||||
private int totalCardsInCollection;
|
||||
private string lastActionMessage = "";
|
||||
|
||||
// UI State
|
||||
private Vector2 scrollPosition;
|
||||
private CardAlbumUI cachedCardAlbumUI;
|
||||
|
||||
[MenuItem("AppleHills/Card System Tester")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<CardSystemTesterWindow>(false, "Card System Tester", true);
|
||||
window.minSize = new Vector2(400, 500);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
private void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
if (state == PlayModeStateChange.EnteredPlayMode)
|
||||
{
|
||||
cachedCardAlbumUI = null;
|
||||
RefreshDebugInfo();
|
||||
}
|
||||
else if (state == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
cachedCardAlbumUI = null;
|
||||
lastActionMessage = "";
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
// Header
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Card System Tester", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox("This tool allows you to test the card system in play mode. " +
|
||||
"Enter play mode to enable the testing functions.", MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
// Test Settings Section
|
||||
DrawTestSettings();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
// Debug Info Section
|
||||
DrawDebugInfo();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
// Test Actions Section
|
||||
DrawTestActions();
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawTestSettings()
|
||||
{
|
||||
EditorGUILayout.LabelField("Test Settings", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!Application.isPlaying);
|
||||
|
||||
boosterPacksToAdd = EditorGUILayout.IntSlider("Booster Packs to Add", boosterPacksToAdd, 1, 10);
|
||||
cardsToGenerate = EditorGUILayout.IntSlider("Cards to Generate", cardsToGenerate, 1, 100);
|
||||
autoOpenPacksWhenAdded = EditorGUILayout.Toggle("Auto-Open Packs When Added", autoOpenPacksWhenAdded);
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
|
||||
private void DrawDebugInfo()
|
||||
{
|
||||
EditorGUILayout.LabelField("Debug Info", EditorStyles.boldLabel);
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.IntField("Current Booster Count", currentBoosterCount);
|
||||
EditorGUILayout.IntField("Total Cards in Collection", totalCardsInCollection);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (!string.IsNullOrEmpty(lastActionMessage))
|
||||
{
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.HelpBox(lastActionMessage, MessageType.None);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
if (GUILayout.Button("Refresh Debug Info"))
|
||||
{
|
||||
RefreshDebugInfo();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox("Debug info available in play mode.", MessageType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTestActions()
|
||||
{
|
||||
EditorGUILayout.LabelField("Test Actions", EditorStyles.boldLabel);
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Enter Play Mode to use these testing functions.", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Booster Pack Actions
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Booster Pack Actions", EditorStyles.miniBoldLabel);
|
||||
|
||||
if (GUILayout.Button("Add Booster Packs", GUILayout.Height(30)))
|
||||
{
|
||||
AddBoosterPacks();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Open Card Menu", GUILayout.Height(30)))
|
||||
{
|
||||
SimulateBackpackClick();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Open Booster Pack", GUILayout.Height(30)))
|
||||
{
|
||||
OpenBoosterPack();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Open Album View", GUILayout.Height(30)))
|
||||
{
|
||||
OpenAlbumView();
|
||||
}
|
||||
|
||||
// Card Generation Actions
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Card Generation Actions", EditorStyles.miniBoldLabel);
|
||||
|
||||
if (GUILayout.Button("Generate Random Cards", GUILayout.Height(30)))
|
||||
{
|
||||
GenerateRandomCards();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Danger Zone
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Danger Zone", EditorStyles.miniBoldLabel);
|
||||
|
||||
GUI.backgroundColor = new Color(1f, 0.6f, 0.6f);
|
||||
if (GUILayout.Button("Clear All Cards", GUILayout.Height(30)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Clear All Cards",
|
||||
"Are you sure you want to clear all cards from the inventory? This cannot be undone in this play session.",
|
||||
"Clear All", "Cancel"))
|
||||
{
|
||||
ClearAllCards();
|
||||
}
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
|
||||
// Refresh the debug information
|
||||
private void RefreshDebugInfo()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
return;
|
||||
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
currentBoosterCount = CardSystemManager.Instance.GetBoosterPackCount();
|
||||
totalCardsInCollection = CardSystemManager.Instance.GetCardInventory().GetAllCards().Count;
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private CardAlbumUI GetCardAlbumUI()
|
||||
{
|
||||
if (cachedCardAlbumUI == null)
|
||||
{
|
||||
cachedCardAlbumUI = Object.FindAnyObjectByType<CardAlbumUI>();
|
||||
|
||||
if (cachedCardAlbumUI == null)
|
||||
{
|
||||
lastActionMessage = "Error: No CardAlbumUI found in the scene!";
|
||||
Debug.LogError("[CardSystemTesterWindow] " + lastActionMessage);
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
return cachedCardAlbumUI;
|
||||
}
|
||||
|
||||
// Test Action Methods
|
||||
|
||||
private void AddBoosterPacks()
|
||||
{
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
CardSystemManager.Instance.AddBoosterPack(boosterPacksToAdd);
|
||||
lastActionMessage = $"Added {boosterPacksToAdd} booster pack(s)";
|
||||
Logging.Debug($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
RefreshDebugInfo();
|
||||
|
||||
if (autoOpenPacksWhenAdded && GetCardAlbumUI() != null)
|
||||
{
|
||||
SimulateBackpackClick();
|
||||
cachedCardAlbumUI.OpenBoosterPack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lastActionMessage = "Error: CardSystemManager instance not found!";
|
||||
Debug.LogError("[CardSystemTesterWindow] " + lastActionMessage);
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void SimulateBackpackClick()
|
||||
{
|
||||
CardAlbumUI cardAlbumUI = GetCardAlbumUI();
|
||||
|
||||
if (cardAlbumUI != null)
|
||||
{
|
||||
if (cardAlbumUI.BackpackIcon != null)
|
||||
{
|
||||
Button backpackButton = cardAlbumUI.BackpackIcon.GetComponent<Button>();
|
||||
if (backpackButton != null)
|
||||
{
|
||||
backpackButton.onClick.Invoke();
|
||||
lastActionMessage = "Opened card menu via backpack click";
|
||||
Logging.Debug($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
}
|
||||
else
|
||||
{
|
||||
lastActionMessage = "Failed to find Button component on backpack icon";
|
||||
Logging.Warning($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lastActionMessage = "BackpackIcon reference is null";
|
||||
Logging.Warning($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenBoosterPack()
|
||||
{
|
||||
CardAlbumUI cardAlbumUI = GetCardAlbumUI();
|
||||
|
||||
if (cardAlbumUI != null)
|
||||
{
|
||||
SimulateBackpackClick();
|
||||
cardAlbumUI.OpenBoosterPack();
|
||||
lastActionMessage = "Opening booster pack";
|
||||
Logging.Debug($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
RefreshDebugInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenAlbumView()
|
||||
{
|
||||
CardAlbumUI cardAlbumUI = GetCardAlbumUI();
|
||||
|
||||
if (cardAlbumUI != null)
|
||||
{
|
||||
SimulateBackpackClick();
|
||||
cardAlbumUI.OpenAlbumView();
|
||||
lastActionMessage = "Opening album view";
|
||||
Logging.Debug($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateRandomCards()
|
||||
{
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
int cardsAdded = 0;
|
||||
List<CardDefinition> allDefinitions = CardSystemManager.Instance.GetAllCardDefinitions();
|
||||
|
||||
if (allDefinitions.Count == 0)
|
||||
{
|
||||
lastActionMessage = "Error: No card definitions available";
|
||||
Logging.Warning($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cardsToGenerate; i++)
|
||||
{
|
||||
// Get a random card definition
|
||||
CardDefinition randomDef = allDefinitions[Random.Range(0, allDefinitions.Count)];
|
||||
|
||||
// Create a card data instance and add it to inventory
|
||||
CardData newCard = randomDef.CreateCardData();
|
||||
CardSystemManager.Instance.GetCardInventory().AddCard(newCard);
|
||||
cardsAdded++;
|
||||
}
|
||||
|
||||
lastActionMessage = $"Generated {cardsAdded} random cards";
|
||||
Logging.Debug($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
RefreshDebugInfo();
|
||||
}
|
||||
else
|
||||
{
|
||||
lastActionMessage = "Error: CardSystemManager instance not found!";
|
||||
Debug.LogError("[CardSystemTesterWindow] " + lastActionMessage);
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearAllCards()
|
||||
{
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
int count = CardSystemManager.Instance.GetCardInventory().GetAllCards().Count;
|
||||
CardSystemManager.Instance.GetCardInventory().ClearAllCards();
|
||||
lastActionMessage = $"Cleared {count} cards from inventory";
|
||||
Logging.Debug($"[CardSystemTesterWindow] {lastActionMessage}");
|
||||
RefreshDebugInfo();
|
||||
}
|
||||
else
|
||||
{
|
||||
lastActionMessage = "Error: CardSystemManager instance not found!";
|
||||
Debug.LogError("[CardSystemTesterWindow] " + lastActionMessage);
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Editor/CardSystem/CardSystemTesterWindow.cs.meta
Normal file
3
Assets/Editor/CardSystem/CardSystemTesterWindow.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8af3bdff6ac2404e91e0ed505d2e617d
|
||||
timeCreated: 1761829755
|
||||
@@ -1,9 +1,9 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
[CustomEditor(typeof(Interactable))]
|
||||
[CustomEditor(typeof(InteractableBase), true)]
|
||||
public class InteractableEditor : UnityEditor.Editor
|
||||
{
|
||||
SerializedProperty isOneTimeProp;
|
||||
@@ -14,6 +14,9 @@ namespace Interactions
|
||||
SerializedProperty characterArrivedProp;
|
||||
SerializedProperty interactionCompleteProp;
|
||||
|
||||
private bool showBaseSettings = true;
|
||||
private bool showEvents = false;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
isOneTimeProp = serializedObject.FindProperty("isOneTime");
|
||||
@@ -29,70 +32,96 @@ namespace Interactions
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.LabelField("Interaction Settings", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(isOneTimeProp);
|
||||
EditorGUILayout.PropertyField(cooldownProp);
|
||||
EditorGUILayout.PropertyField(characterToInteractProp);
|
||||
// Draw child-specific properties first (anything not part of base class)
|
||||
DrawPropertiesExcluding(serializedObject,
|
||||
"m_Script",
|
||||
"isOneTime",
|
||||
"cooldown",
|
||||
"characterToInteract",
|
||||
"interactionStarted",
|
||||
"interactionInterrupted",
|
||||
"characterArrived",
|
||||
"interactionComplete");
|
||||
|
||||
// Add the buttons for creating move targets
|
||||
// Base Interaction Settings (Collapsible)
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Character Move Targets", EditorStyles.boldLabel);
|
||||
showBaseSettings = EditorGUILayout.Foldout(showBaseSettings, "Base Interaction Settings", true, EditorStyles.foldoutHeader);
|
||||
if (showBaseSettings)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(isOneTimeProp);
|
||||
EditorGUILayout.PropertyField(cooldownProp);
|
||||
EditorGUILayout.PropertyField(characterToInteractProp);
|
||||
|
||||
// Character Move Targets (sub-section)
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Character Move Targets", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Add Trafalgar Target"))
|
||||
{
|
||||
CreateMoveTarget(CharacterToInteract.Trafalgar);
|
||||
}
|
||||
if (GUILayout.Button("Add Pulver Target"))
|
||||
{
|
||||
CreateMoveTarget(CharacterToInteract.Pulver);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// Add a button for creating a "Both" target
|
||||
if (GUILayout.Button("Add Both Characters Target"))
|
||||
{
|
||||
CreateMoveTarget(CharacterToInteract.Both);
|
||||
}
|
||||
|
||||
// Display character target counts
|
||||
Interactable interactable = (Interactable)target;
|
||||
CharacterMoveToTarget[] moveTargets = interactable.GetComponentsInChildren<CharacterMoveToTarget>();
|
||||
int trafalgarTargets = 0;
|
||||
int pulverTargets = 0;
|
||||
int bothTargets = 0;
|
||||
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == CharacterToInteract.Trafalgar)
|
||||
trafalgarTargets++;
|
||||
else if (target.characterType == CharacterToInteract.Pulver)
|
||||
pulverTargets++;
|
||||
else if (target.characterType == CharacterToInteract.Both)
|
||||
bothTargets++;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField($"Trafalgar Targets: {trafalgarTargets}, Pulver Targets: {pulverTargets}, Both Targets: {bothTargets}");
|
||||
|
||||
if (trafalgarTargets > 1 || pulverTargets > 1 || bothTargets > 1 ||
|
||||
(bothTargets > 0 && (trafalgarTargets > 0 || pulverTargets > 0)))
|
||||
{
|
||||
EditorGUILayout.HelpBox("Warning: Multiple move targets found that may conflict. Priority order: Both > Character-specific targets.", MessageType.Warning);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Add Trafalgar Target"))
|
||||
{
|
||||
CreateMoveTarget(CharacterToInteract.Trafalgar);
|
||||
}
|
||||
if (GUILayout.Button("Add Pulver Target"))
|
||||
{
|
||||
CreateMoveTarget(CharacterToInteract.Pulver);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// Add a button for creating a "Both" target
|
||||
if (GUILayout.Button("Add Both Characters Target"))
|
||||
{
|
||||
CreateMoveTarget(CharacterToInteract.Both);
|
||||
}
|
||||
|
||||
// Display character target counts
|
||||
InteractableBase interactable = (InteractableBase)target;
|
||||
CharacterMoveToTarget[] moveTargets = interactable.GetComponentsInChildren<CharacterMoveToTarget>();
|
||||
int trafalgarTargets = 0;
|
||||
int pulverTargets = 0;
|
||||
int bothTargets = 0;
|
||||
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == CharacterToInteract.Trafalgar)
|
||||
trafalgarTargets++;
|
||||
else if (target.characterType == CharacterToInteract.Pulver)
|
||||
pulverTargets++;
|
||||
else if (target.characterType == CharacterToInteract.Both)
|
||||
bothTargets++;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField($"Trafalgar Targets: {trafalgarTargets}, Pulver Targets: {pulverTargets}, Both Targets: {bothTargets}");
|
||||
|
||||
if (trafalgarTargets > 1 || pulverTargets > 1 || bothTargets > 1 ||
|
||||
(bothTargets > 0 && (trafalgarTargets > 0 || pulverTargets > 0)))
|
||||
{
|
||||
EditorGUILayout.HelpBox("Warning: Multiple move targets found that may conflict. Priority order: Both > Character-specific targets.", MessageType.Warning);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
// Interaction Events (Collapsible)
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Interaction Events", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(interactionStartedProp);
|
||||
EditorGUILayout.PropertyField(interactionInterruptedProp);
|
||||
EditorGUILayout.PropertyField(characterArrivedProp);
|
||||
EditorGUILayout.PropertyField(interactionCompleteProp);
|
||||
showEvents = EditorGUILayout.Foldout(showEvents, "Interaction Events", true, EditorStyles.foldoutHeader);
|
||||
if (showEvents)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(interactionStartedProp);
|
||||
EditorGUILayout.PropertyField(interactionInterruptedProp);
|
||||
EditorGUILayout.PropertyField(characterArrivedProp);
|
||||
EditorGUILayout.PropertyField(interactionCompleteProp);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private void CreateMoveTarget(CharacterToInteract characterType)
|
||||
{
|
||||
Interactable interactable = (Interactable)target;
|
||||
InteractableBase interactable = (InteractableBase)target;
|
||||
|
||||
// Create a new GameObject
|
||||
GameObject targetObj = new GameObject($"{characterType}MoveTarget");
|
||||
|
||||
649
Assets/Editor/RemoveInteractableBaseComponents.cs
Normal file
649
Assets/Editor/RemoveInteractableBaseComponents.cs
Normal file
@@ -0,0 +1,649 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine.SceneManagement;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Interactions;
|
||||
|
||||
namespace Editor
|
||||
{
|
||||
public class RemoveInteractableBaseComponents : EditorWindow
|
||||
{
|
||||
private List<string> problematicPrefabs = new List<string>();
|
||||
private List<string> problematicScenes = new List<string>();
|
||||
private Vector2 scrollPosition;
|
||||
private bool hasScanned;
|
||||
private int componentsFound;
|
||||
|
||||
[MenuItem("AppleHills/Remove InteractableBase Components")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<RemoveInteractableBaseComponents>("Remove InteractableBase");
|
||||
window.minSize = new Vector2(700, 500);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.Label("Remove InteractableBase Component References", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"This tool finds and removes EXACT InteractableBase components from prefabs and scenes.\n\n" +
|
||||
"Only finds the bare base class, NOT derived types like Pickup/ItemSlot/OneClickInteraction.\n\n" +
|
||||
"If components depend on InteractableBase, you'll be prompted to replace it.",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Scan All Prefabs and Scenes", GUILayout.Height(35)))
|
||||
{
|
||||
ScanAll();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (hasScanned)
|
||||
{
|
||||
EditorGUILayout.LabelField($"Found {componentsFound} exact InteractableBase components", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField($"In {problematicPrefabs.Count} prefabs");
|
||||
EditorGUILayout.LabelField($"In {problematicScenes.Count} scenes");
|
||||
|
||||
if (componentsFound > 0)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
if (problematicPrefabs.Count > 0 && GUILayout.Button($"Remove from Prefabs ({problematicPrefabs.Count})", GUILayout.Height(35)))
|
||||
{
|
||||
RemoveFromAllPrefabs();
|
||||
}
|
||||
|
||||
if (problematicScenes.Count > 0 && GUILayout.Button($"Remove from Scenes ({problematicScenes.Count})", GUILayout.Height(35)))
|
||||
{
|
||||
RemoveFromAllScenes();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Remove All (Prefabs + Scenes)", GUILayout.Height(35)))
|
||||
{
|
||||
RemoveAll();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
if (problematicPrefabs.Count > 0)
|
||||
{
|
||||
EditorGUILayout.LabelField("Prefabs:", EditorStyles.boldLabel);
|
||||
foreach (var prefabPath in problematicPrefabs)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal("box");
|
||||
EditorGUILayout.LabelField(prefabPath);
|
||||
if (GUILayout.Button("Remove", GUILayout.Width(80)))
|
||||
{
|
||||
RemoveFromPrefab(prefabPath);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
if (problematicScenes.Count > 0)
|
||||
{
|
||||
EditorGUILayout.LabelField("Scenes:", EditorStyles.boldLabel);
|
||||
foreach (var scenePath in problematicScenes)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal("box");
|
||||
EditorGUILayout.LabelField(scenePath);
|
||||
if (GUILayout.Button("Remove", GUILayout.Width(80)))
|
||||
{
|
||||
RemoveFromScene(scenePath);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox("No exact InteractableBase components found! All clean.", MessageType.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanAll()
|
||||
{
|
||||
problematicPrefabs.Clear();
|
||||
problematicScenes.Clear();
|
||||
componentsFound = 0;
|
||||
hasScanned = true;
|
||||
|
||||
ScanPrefabs();
|
||||
ScanScenes();
|
||||
|
||||
Debug.Log($"<color=cyan>[Scan Complete]</color> Found {componentsFound} exact InteractableBase components in {problematicPrefabs.Count} prefabs and {problematicScenes.Count} scenes.");
|
||||
}
|
||||
|
||||
private void ScanPrefabs()
|
||||
{
|
||||
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
|
||||
|
||||
EditorUtility.DisplayProgressBar("Scanning Prefabs", "Starting...", 0f);
|
||||
|
||||
for (int i = 0; i < prefabGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
||||
|
||||
EditorUtility.DisplayProgressBar("Scanning Prefabs",
|
||||
$"Checking {i + 1}/{prefabGuids.Length}: {Path.GetFileName(path)}",
|
||||
(float)i / prefabGuids.Length);
|
||||
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (prefab != null)
|
||||
{
|
||||
// Check if this prefab or any of its children have EXACTLY InteractableBase (not derived types)
|
||||
InteractableBase[] components = prefab.GetComponentsInChildren<InteractableBase>(true);
|
||||
int exactMatches = 0;
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component != null && component.GetType() == typeof(InteractableBase))
|
||||
{
|
||||
exactMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
if (exactMatches > 0)
|
||||
{
|
||||
problematicPrefabs.Add(path);
|
||||
componentsFound += exactMatches;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
private void ScanScenes()
|
||||
{
|
||||
string[] sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets/Scenes" });
|
||||
|
||||
EditorUtility.DisplayProgressBar("Scanning Scenes", "Starting...", 0f);
|
||||
|
||||
string currentScenePath = SceneManager.GetActiveScene().path;
|
||||
|
||||
for (int i = 0; i < sceneGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(sceneGuids[i]);
|
||||
|
||||
EditorUtility.DisplayProgressBar("Scanning Scenes",
|
||||
$"Checking {i + 1}/{sceneGuids.Length}: {Path.GetFileName(path)}",
|
||||
(float)i / sceneGuids.Length);
|
||||
|
||||
EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
|
||||
|
||||
// Find all InteractableBase components in the scene
|
||||
InteractableBase[] components = GameObject.FindObjectsByType<InteractableBase>(FindObjectsSortMode.None);
|
||||
|
||||
int exactMatches = 0;
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component != null && component.GetType() == typeof(InteractableBase))
|
||||
{
|
||||
exactMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
if (exactMatches > 0)
|
||||
{
|
||||
problematicScenes.Add(path);
|
||||
componentsFound += exactMatches;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original scene
|
||||
if (!string.IsNullOrEmpty(currentScenePath))
|
||||
{
|
||||
EditorSceneManager.OpenScene(currentScenePath);
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
private void RemoveFromAllPrefabs()
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Confirm Removal",
|
||||
$"This will remove InteractableBase components from {problematicPrefabs.Count} prefabs.\n\n" +
|
||||
"This cannot be undone (unless you use version control).\n\nContinue?",
|
||||
"Yes, Remove", "Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
|
||||
for (int i = 0; i < problematicPrefabs.Count; i++)
|
||||
{
|
||||
string path = problematicPrefabs[i];
|
||||
|
||||
EditorUtility.DisplayProgressBar("Removing Components from Prefabs",
|
||||
$"Processing {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}",
|
||||
(float)i / problematicPrefabs.Count);
|
||||
|
||||
removedCount += RemoveFromPrefab(path);
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"<color=green>[Prefab Cleanup Complete]</color> Removed {removedCount} InteractableBase components from prefabs.");
|
||||
|
||||
ScanAll();
|
||||
}
|
||||
|
||||
private void RemoveFromAllScenes()
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Confirm Removal",
|
||||
$"This will remove InteractableBase components from {problematicScenes.Count} scenes.\n\n" +
|
||||
"This cannot be undone (unless you use version control).\n\nContinue?",
|
||||
"Yes, Remove", "Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
string currentScenePath = SceneManager.GetActiveScene().path;
|
||||
|
||||
for (int i = 0; i < problematicScenes.Count; i++)
|
||||
{
|
||||
string path = problematicScenes[i];
|
||||
|
||||
EditorUtility.DisplayProgressBar("Removing Components from Scenes",
|
||||
$"Processing {i + 1}/{problematicScenes.Count}: {Path.GetFileName(path)}",
|
||||
(float)i / problematicScenes.Count);
|
||||
|
||||
removedCount += RemoveFromScene(path);
|
||||
}
|
||||
|
||||
// Restore original scene
|
||||
if (!string.IsNullOrEmpty(currentScenePath))
|
||||
{
|
||||
EditorSceneManager.OpenScene(currentScenePath);
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"<color=green>[Scene Cleanup Complete]</color> Removed {removedCount} InteractableBase components from scenes.");
|
||||
|
||||
ScanAll();
|
||||
}
|
||||
|
||||
private void RemoveAll()
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Confirm Removal",
|
||||
$"This will remove {componentsFound} InteractableBase components from:\n" +
|
||||
$"• {problematicPrefabs.Count} prefabs\n" +
|
||||
$"• {problematicScenes.Count} scenes\n\n" +
|
||||
"This cannot be undone (unless you use version control).\n\nContinue?",
|
||||
"Yes, Remove", "Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
|
||||
// Remove from prefabs
|
||||
for (int i = 0; i < problematicPrefabs.Count; i++)
|
||||
{
|
||||
string path = problematicPrefabs[i];
|
||||
|
||||
EditorUtility.DisplayProgressBar("Removing Components",
|
||||
$"Prefabs {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}",
|
||||
(float)i / (problematicPrefabs.Count + problematicScenes.Count));
|
||||
|
||||
removedCount += RemoveFromPrefab(path);
|
||||
}
|
||||
|
||||
// Remove from scenes
|
||||
string currentScenePath = SceneManager.GetActiveScene().path;
|
||||
|
||||
for (int i = 0; i < problematicScenes.Count; i++)
|
||||
{
|
||||
string path = problematicScenes[i];
|
||||
|
||||
EditorUtility.DisplayProgressBar("Removing Components",
|
||||
$"Scenes {i + 1}/{problematicScenes.Count}: {Path.GetFileName(path)}",
|
||||
(float)(problematicPrefabs.Count + i) / (problematicPrefabs.Count + problematicScenes.Count));
|
||||
|
||||
removedCount += RemoveFromScene(path);
|
||||
}
|
||||
|
||||
// Restore original scene
|
||||
if (!string.IsNullOrEmpty(currentScenePath))
|
||||
{
|
||||
EditorSceneManager.OpenScene(currentScenePath);
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"<color=green>[Removal Complete]</color> Removed {removedCount} InteractableBase components.");
|
||||
|
||||
ScanAll();
|
||||
}
|
||||
|
||||
private int RemoveFromPrefab(string assetPath)
|
||||
{
|
||||
int removed = 0;
|
||||
|
||||
try
|
||||
{
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"Could not load prefab at path: {assetPath}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
string prefabPath = AssetDatabase.GetAssetPath(prefab);
|
||||
GameObject prefabContents = null;
|
||||
|
||||
try
|
||||
{
|
||||
prefabContents = PrefabUtility.LoadPrefabContents(prefabPath);
|
||||
}
|
||||
catch (Exception loadEx)
|
||||
{
|
||||
Debug.LogError($"Failed to load prefab contents for {assetPath}: {loadEx.Message}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (prefabContents == null)
|
||||
{
|
||||
Debug.LogWarning($"Prefab contents are null for: {assetPath}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
InteractableBase[] components = prefabContents.GetComponentsInChildren<InteractableBase>(true);
|
||||
|
||||
if (components == null || components.Length == 0)
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(prefabContents);
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component == null)
|
||||
continue;
|
||||
|
||||
// Check if it's EXACTLY InteractableBase (not a derived type)
|
||||
if (component.GetType() == typeof(InteractableBase))
|
||||
{
|
||||
// Cache references before destroying
|
||||
GameObject targetObject = component.gameObject;
|
||||
string objectName = targetObject != null ? targetObject.name : "Unknown";
|
||||
|
||||
// Check if GameObject already has a derived InteractableBase type
|
||||
bool hasPickup = targetObject.GetComponent<Pickup>() != null;
|
||||
bool hasItemSlot = targetObject.GetComponent<ItemSlot>() != null;
|
||||
bool hasOneClick = targetObject.GetComponent<OneClickInteraction>() != null;
|
||||
|
||||
if (hasPickup || hasItemSlot || hasOneClick)
|
||||
{
|
||||
// GameObject already has a concrete type, safe to remove bare base class
|
||||
DestroyImmediate(component);
|
||||
removed++;
|
||||
string existingType = hasItemSlot ? "ItemSlot" : (hasPickup ? "Pickup" : "OneClickInteraction");
|
||||
Debug.Log($"<color=green>[Removed]</color> Bare InteractableBase from '{objectName}' (already has {existingType}) in prefab '{Path.GetFileName(assetPath)}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check what other components depend on InteractableBase
|
||||
Component[] allComponents = targetObject.GetComponents<Component>();
|
||||
List<string> dependentComponents = new List<string>();
|
||||
|
||||
foreach (var otherComponent in allComponents)
|
||||
{
|
||||
if (otherComponent == null || otherComponent == component)
|
||||
continue;
|
||||
|
||||
var requireAttributes = otherComponent.GetType().GetCustomAttributes(typeof(RequireComponent), true);
|
||||
foreach (RequireComponent attr in requireAttributes)
|
||||
{
|
||||
if (attr.m_Type0 == typeof(InteractableBase) ||
|
||||
attr.m_Type1 == typeof(InteractableBase) ||
|
||||
attr.m_Type2 == typeof(InteractableBase))
|
||||
{
|
||||
dependentComponents.Add(otherComponent.GetType().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dependentComponents.Count > 0)
|
||||
{
|
||||
string dependencyList = string.Join(", ", dependentComponents);
|
||||
string message = $"GameObject '{objectName}' in prefab '{Path.GetFileName(assetPath)}' has InteractableBase, " +
|
||||
$"but these components depend on it:\n\n{dependencyList}\n\n" +
|
||||
"Replace InteractableBase with:";
|
||||
|
||||
int choice = EditorUtility.DisplayDialogComplex(
|
||||
"Component Dependency Detected",
|
||||
message,
|
||||
"Pickup",
|
||||
"ItemSlot",
|
||||
"OneClickInteraction");
|
||||
|
||||
Type replacementType = choice switch
|
||||
{
|
||||
0 => typeof(Pickup),
|
||||
1 => typeof(ItemSlot),
|
||||
2 => typeof(OneClickInteraction),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (replacementType != null)
|
||||
{
|
||||
// Cache component data before destroying
|
||||
bool isOneTime = component.isOneTime;
|
||||
float cooldown = component.cooldown;
|
||||
CharacterToInteract characterToInteract = component.characterToInteract;
|
||||
|
||||
DestroyImmediate(component);
|
||||
|
||||
var newComponent = targetObject.AddComponent(replacementType) as InteractableBase;
|
||||
if (newComponent != null)
|
||||
{
|
||||
newComponent.isOneTime = isOneTime;
|
||||
newComponent.cooldown = cooldown;
|
||||
newComponent.characterToInteract = characterToInteract;
|
||||
|
||||
removed++;
|
||||
Debug.Log($"<color=cyan>[Replaced]</color> InteractableBase with {replacementType.Name} on '{objectName}' in prefab '{Path.GetFileName(assetPath)}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Skipped removing InteractableBase from '{objectName}' - no replacement chosen");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(component);
|
||||
removed++;
|
||||
Debug.Log($"<color=yellow>[Removed]</color> InteractableBase from '{objectName}' in prefab '{Path.GetFileName(assetPath)}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
PrefabUtility.SaveAsPrefabAsset(prefabContents, prefabPath);
|
||||
}
|
||||
catch (Exception saveEx)
|
||||
{
|
||||
Debug.LogError($"Failed to save prefab {assetPath}: {saveEx.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
PrefabUtility.UnloadPrefabContents(prefabContents);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Error removing components from prefab {assetPath}: {ex.Message}\nStack: {ex.StackTrace}");
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
private int RemoveFromScene(string scenePath)
|
||||
{
|
||||
int removed = 0;
|
||||
|
||||
try
|
||||
{
|
||||
Scene scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
|
||||
if (!scene.isLoaded)
|
||||
{
|
||||
Debug.LogWarning($"Scene not loaded: {scenePath}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
InteractableBase[] components = GameObject.FindObjectsByType<InteractableBase>(FindObjectsSortMode.None);
|
||||
|
||||
if (components == null || components.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component == null)
|
||||
continue;
|
||||
|
||||
if (component.GetType() == typeof(InteractableBase))
|
||||
{
|
||||
// Cache references before destroying
|
||||
GameObject targetObject = component.gameObject;
|
||||
string objectName = targetObject != null ? targetObject.name : "Unknown";
|
||||
|
||||
// Check if GameObject already has a derived InteractableBase type
|
||||
bool hasPickup = targetObject.GetComponent<Pickup>() != null;
|
||||
bool hasItemSlot = targetObject.GetComponent<ItemSlot>() != null;
|
||||
bool hasOneClick = targetObject.GetComponent<OneClickInteraction>() != null;
|
||||
|
||||
if (hasPickup || hasItemSlot || hasOneClick)
|
||||
{
|
||||
// GameObject already has a concrete type, safe to remove bare base class
|
||||
DestroyImmediate(component);
|
||||
removed++;
|
||||
string existingType = hasItemSlot ? "ItemSlot" : (hasPickup ? "Pickup" : "OneClickInteraction");
|
||||
Debug.Log($"<color=green>[Removed]</color> Bare InteractableBase from '{objectName}' (already has {existingType}) in scene '{Path.GetFileName(scenePath)}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
Component[] allComponents = targetObject.GetComponents<Component>();
|
||||
List<string> dependentComponents = new List<string>();
|
||||
|
||||
foreach (var otherComponent in allComponents)
|
||||
{
|
||||
if (otherComponent == null || otherComponent == component)
|
||||
continue;
|
||||
|
||||
var requireAttributes = otherComponent.GetType().GetCustomAttributes(typeof(RequireComponent), true);
|
||||
foreach (RequireComponent attr in requireAttributes)
|
||||
{
|
||||
if (attr.m_Type0 == typeof(InteractableBase) ||
|
||||
attr.m_Type1 == typeof(InteractableBase) ||
|
||||
attr.m_Type2 == typeof(InteractableBase))
|
||||
{
|
||||
dependentComponents.Add(otherComponent.GetType().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dependentComponents.Count > 0)
|
||||
{
|
||||
string dependencyList = string.Join(", ", dependentComponents);
|
||||
string message = $"GameObject '{objectName}' in scene '{Path.GetFileName(scenePath)}' has InteractableBase, " +
|
||||
$"but these components depend on it:\n\n{dependencyList}\n\n" +
|
||||
"Replace InteractableBase with:";
|
||||
|
||||
int choice = EditorUtility.DisplayDialogComplex(
|
||||
"Component Dependency Detected",
|
||||
message,
|
||||
"Pickup",
|
||||
"ItemSlot",
|
||||
"OneClickInteraction");
|
||||
|
||||
Type replacementType = choice switch
|
||||
{
|
||||
0 => typeof(Pickup),
|
||||
1 => typeof(ItemSlot),
|
||||
2 => typeof(OneClickInteraction),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (replacementType != null)
|
||||
{
|
||||
// Cache component data before destroying
|
||||
bool isOneTime = component.isOneTime;
|
||||
float cooldown = component.cooldown;
|
||||
CharacterToInteract characterToInteract = component.characterToInteract;
|
||||
|
||||
DestroyImmediate(component);
|
||||
|
||||
var newComponent = targetObject.AddComponent(replacementType) as InteractableBase;
|
||||
if (newComponent != null)
|
||||
{
|
||||
newComponent.isOneTime = isOneTime;
|
||||
newComponent.cooldown = cooldown;
|
||||
newComponent.characterToInteract = characterToInteract;
|
||||
|
||||
removed++;
|
||||
Debug.Log($"<color=cyan>[Replaced]</color> InteractableBase with {replacementType.Name} on '{objectName}' in scene '{Path.GetFileName(scenePath)}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Skipped removing InteractableBase from '{objectName}' - no replacement chosen");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(component);
|
||||
removed++;
|
||||
Debug.Log($"<color=yellow>[Removed]</color> InteractableBase from '{objectName}' in scene '{Path.GetFileName(scenePath)}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
EditorSceneManager.SaveScene(scene);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Error removing components from scene {scenePath}: {ex.Message}\nStack: {ex.StackTrace}");
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Editor/RemoveInteractableBaseComponents.cs.meta
Normal file
3
Assets/Editor/RemoveInteractableBaseComponents.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da895c0622e34ef8a18675993eec9877
|
||||
timeCreated: 1762024152
|
||||
228
Assets/Editor/RemoveOldInteractableReferences.cs
Normal file
228
Assets/Editor/RemoveOldInteractableReferences.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Editor
|
||||
{
|
||||
public class RemoveOldInteractableReferences : EditorWindow
|
||||
{
|
||||
private List<string> problematicPrefabs = new List<string>();
|
||||
private Vector2 scrollPosition;
|
||||
private bool hasScanned = false;
|
||||
|
||||
[MenuItem("AppleHills/Remove Old Interactable References")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<RemoveOldInteractableReferences>("Clean Old Interactables");
|
||||
window.minSize = new Vector2(600, 400);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.Label("Remove Old Interactable/InteractableBase References", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"This tool finds and removes references to:\n" +
|
||||
"- Interactable (old script name)\n" +
|
||||
"- InteractableBase (abstract class - not allowed on prefabs)\n\n" +
|
||||
"These should be replaced by concrete types: Pickup, ItemSlot, or OneClickInteraction",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Scan All Prefabs", GUILayout.Height(30)))
|
||||
{
|
||||
ScanPrefabs();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (hasScanned)
|
||||
{
|
||||
EditorGUILayout.LabelField($"Found {problematicPrefabs.Count} prefabs with old references", EditorStyles.boldLabel);
|
||||
|
||||
if (problematicPrefabs.Count > 0)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Clean All Prefabs", GUILayout.Height(30)))
|
||||
{
|
||||
CleanAllPrefabs();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
foreach (var prefabPath in problematicPrefabs)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal("box");
|
||||
EditorGUILayout.LabelField(prefabPath);
|
||||
if (GUILayout.Button("Clean This", GUILayout.Width(80)))
|
||||
{
|
||||
CleanSinglePrefab(prefabPath);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox("No problematic prefabs found! All clean.", MessageType.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanPrefabs()
|
||||
{
|
||||
problematicPrefabs.Clear();
|
||||
hasScanned = true;
|
||||
|
||||
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
|
||||
|
||||
EditorUtility.DisplayProgressBar("Scanning Prefabs", "Starting...", 0f);
|
||||
|
||||
for (int i = 0; i < prefabGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
||||
|
||||
EditorUtility.DisplayProgressBar("Scanning Prefabs",
|
||||
$"Checking {i + 1}/{prefabGuids.Length}: {Path.GetFileName(path)}",
|
||||
(float)i / prefabGuids.Length);
|
||||
|
||||
if (PrefabHasOldInteractableReference(path))
|
||||
{
|
||||
problematicPrefabs.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
Debug.Log($"<color=cyan>[Scan Complete]</color> Found {problematicPrefabs.Count} prefabs with old Interactable/InteractableBase references.");
|
||||
}
|
||||
|
||||
private bool PrefabHasOldInteractableReference(string assetPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string fullPath = Path.GetFullPath(assetPath);
|
||||
string content = File.ReadAllText(fullPath);
|
||||
|
||||
// Look for GUID of Interactable script (11500000 is MonoBehaviour type)
|
||||
// We're looking for the script reference pattern in YAML
|
||||
// Pattern: m_Script: {fileID: 11500000, guid: SCRIPT_GUID, type: 3}
|
||||
|
||||
// Check if content contains "Interactable" class name references
|
||||
// This is a simple text search - if the YAML contains these class names, it likely references them
|
||||
if (content.Contains("InteractableBase") ||
|
||||
(content.Contains("Interactable") && !content.Contains("OneClickInteraction")))
|
||||
{
|
||||
// Additional check: Look for MonoBehaviour blocks with missing scripts (fileID: 0)
|
||||
if (Regex.IsMatch(content, @"m_Script:\s*\{fileID:\s*0\}"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for direct class name matches in script references
|
||||
if (Regex.IsMatch(content, @"m_Name:\s*(Interactable|InteractableBase)"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Error scanning {assetPath}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanAllPrefabs()
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Confirm Cleanup",
|
||||
$"This will remove old Interactable/InteractableBase references from {problematicPrefabs.Count} prefabs.\n\nThis cannot be undone (unless you use version control).\n\nContinue?",
|
||||
"Yes, Clean", "Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int cleanedCount = 0;
|
||||
|
||||
for (int i = 0; i < problematicPrefabs.Count; i++)
|
||||
{
|
||||
string path = problematicPrefabs[i];
|
||||
|
||||
EditorUtility.DisplayProgressBar("Cleaning Prefabs",
|
||||
$"Cleaning {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}",
|
||||
(float)i / problematicPrefabs.Count);
|
||||
|
||||
if (CleanPrefabFile(path))
|
||||
{
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"<color=green>[Cleanup Complete]</color> Cleaned {cleanedCount} prefabs.");
|
||||
|
||||
// Re-scan to update the list
|
||||
ScanPrefabs();
|
||||
}
|
||||
|
||||
private void CleanSinglePrefab(string assetPath)
|
||||
{
|
||||
if (CleanPrefabFile(assetPath))
|
||||
{
|
||||
Debug.Log($"<color=green>[Cleaned]</color> {assetPath}");
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
// Re-scan to update the list
|
||||
ScanPrefabs();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CleanPrefabFile(string assetPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string fullPath = Path.GetFullPath(assetPath);
|
||||
string content = File.ReadAllText(fullPath);
|
||||
string originalContent = content;
|
||||
|
||||
// Pattern 1: Remove entire MonoBehaviour component blocks with missing scripts (fileID: 0)
|
||||
// This removes the component header and all its properties until the next component or end
|
||||
string missingScriptPattern = @"--- !u!114 &\d+\r?\nMonoBehaviour:(?:\r?\n(?!---).+)*?\r?\n m_Script: \{fileID: 0\}(?:\r?\n(?!---).+)*";
|
||||
content = Regex.Replace(content, missingScriptPattern, "", RegexOptions.Multiline);
|
||||
|
||||
// Pattern 2: Remove MonoBehaviour blocks that explicitly reference InteractableBase or Interactable
|
||||
// This is more aggressive and targets the class name directly
|
||||
string interactablePattern = @"--- !u!114 &\d+\r?\nMonoBehaviour:(?:\r?\n(?!---).+)*?\r?\n m_Name: (?:Interactable|InteractableBase)(?:\r?\n(?!---).+)*";
|
||||
content = Regex.Replace(content, interactablePattern, "", RegexOptions.Multiline);
|
||||
|
||||
if (content != originalContent)
|
||||
{
|
||||
// Clean up any double blank lines that might have been created
|
||||
content = Regex.Replace(content, @"(\r?\n){3,}", "\n\n");
|
||||
|
||||
File.WriteAllText(fullPath, content);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"Error cleaning {assetPath}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Editor/RemoveOldInteractableReferences.cs.meta
Normal file
3
Assets/Editor/RemoveOldInteractableReferences.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d898bc44012542c0942b632b56cea3dc
|
||||
timeCreated: 1762023714
|
||||
575
Assets/Editor/StateMachineMigrationTool.cs
Normal file
575
Assets/Editor/StateMachineMigrationTool.cs
Normal file
@@ -0,0 +1,575 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using Core.SaveLoad;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor utility to migrate StateMachine components to SaveableStateMachine.
|
||||
/// </summary>
|
||||
public class StateMachineMigrationTool : EditorWindow
|
||||
{
|
||||
private Vector2 scrollPosition;
|
||||
private List<StateMachineInfo> foundStateMachines = new List<StateMachineInfo>();
|
||||
private bool showPrefabs = true;
|
||||
private bool showScenes = true;
|
||||
|
||||
[MenuItem("Tools/AppleHills/Migrate StateMachines to Saveable")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<StateMachineMigrationTool>("StateMachine Migration");
|
||||
window.minSize = new Vector2(600, 400);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("StateMachine → SaveableStateMachine Migration", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"This tool will replace all StateMachine components with SaveableStateMachine.\n" +
|
||||
"All properties and references will be preserved.",
|
||||
MessageType.Info
|
||||
);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Scan options
|
||||
EditorGUILayout.LabelField("Scan Options:", EditorStyles.boldLabel);
|
||||
showPrefabs = EditorGUILayout.Toggle("Include Prefabs", showPrefabs);
|
||||
showScenes = EditorGUILayout.Toggle("Include Scenes", showScenes);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Scan Project", GUILayout.Height(30)))
|
||||
{
|
||||
ScanProject();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Display results
|
||||
if (foundStateMachines.Count > 0)
|
||||
{
|
||||
EditorGUILayout.LabelField($"Found {foundStateMachines.Count} StateMachine(s):", EditorStyles.boldLabel);
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
foreach (var info in foundStateMachines)
|
||||
{
|
||||
// Set background color for migrated items
|
||||
Color originalBgColor = GUI.backgroundColor;
|
||||
if (info.isAlreadySaveable)
|
||||
{
|
||||
GUI.backgroundColor = new Color(0.5f, 1f, 0.5f); // Light green
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginHorizontal("box");
|
||||
|
||||
GUI.backgroundColor = originalBgColor; // Reset for content
|
||||
|
||||
EditorGUILayout.LabelField(info.name, GUILayout.Width(200));
|
||||
EditorGUILayout.LabelField(info.path, GUILayout.ExpandWidth(true));
|
||||
|
||||
// Ping button (for scene objects or prefabs)
|
||||
if (GUILayout.Button("Ping", GUILayout.Width(50)))
|
||||
{
|
||||
PingObject(info);
|
||||
}
|
||||
|
||||
if (info.isAlreadySaveable)
|
||||
{
|
||||
// Green checkmark with bold style
|
||||
GUIStyle greenStyle = new GUIStyle(GUI.skin.label);
|
||||
greenStyle.normal.textColor = new Color(0f, 0.6f, 0f);
|
||||
greenStyle.fontStyle = FontStyle.Bold;
|
||||
EditorGUILayout.LabelField("✓ Migrated", greenStyle, GUILayout.Width(120));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("Migrate", GUILayout.Width(80)))
|
||||
{
|
||||
if (MigrateSingle(info))
|
||||
{
|
||||
// Refresh the list to show updated status
|
||||
ScanProject();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUI.backgroundColor = originalBgColor; // Final reset
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
int migrateCount = foundStateMachines.Count(sm => !sm.isAlreadySaveable);
|
||||
if (migrateCount > 0)
|
||||
{
|
||||
if (GUILayout.Button($"Migrate All ({migrateCount})", GUILayout.Height(40)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Confirm Migration",
|
||||
$"This will migrate {migrateCount} StateMachine(s) to SaveableStateMachine.\n\nContinue?",
|
||||
"Yes, Migrate All",
|
||||
"Cancel"))
|
||||
{
|
||||
MigrateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox("Click 'Scan Project' to find StateMachine components.", MessageType.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanProject()
|
||||
{
|
||||
foundStateMachines.Clear();
|
||||
|
||||
// Find all prefabs
|
||||
if (showPrefabs)
|
||||
{
|
||||
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
||||
foreach (string guid in prefabGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
|
||||
if (prefab != null)
|
||||
{
|
||||
// Use GetComponents to find Pixelplacement.StateMachine
|
||||
var components = prefab.GetComponentsInChildren<Component>(true);
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component == null) continue;
|
||||
|
||||
var componentType = component.GetType();
|
||||
if (componentType.Name == "StateMachine" && componentType.Namespace == "Pixelplacement")
|
||||
{
|
||||
bool isAlreadySaveable = component is AppleMachine;
|
||||
foundStateMachines.Add(new StateMachineInfo
|
||||
{
|
||||
name = component.gameObject.name,
|
||||
path = path,
|
||||
isPrefab = true,
|
||||
isAlreadySaveable = isAlreadySaveable,
|
||||
assetPath = path
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all in scenes
|
||||
if (showScenes)
|
||||
{
|
||||
string[] sceneGuids = AssetDatabase.FindAssets("t:Scene");
|
||||
foreach (string guid in sceneGuids)
|
||||
{
|
||||
string scenePath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
|
||||
|
||||
var allComponents = GameObject.FindObjectsOfType<Component>(true);
|
||||
foreach (var component in allComponents)
|
||||
{
|
||||
if (component == null || component.gameObject.scene != scene) continue;
|
||||
|
||||
var componentType = component.GetType();
|
||||
if (componentType.Name == "StateMachine" && componentType.Namespace == "Pixelplacement")
|
||||
{
|
||||
bool isAlreadySaveable = component is AppleMachine;
|
||||
foundStateMachines.Add(new StateMachineInfo
|
||||
{
|
||||
name = component.gameObject.name,
|
||||
path = $"{scenePath} → {component.transform.GetHierarchyPath()}",
|
||||
isPrefab = false,
|
||||
isAlreadySaveable = isAlreadySaveable,
|
||||
assetPath = scenePath,
|
||||
gameObject = component.gameObject
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[StateMachine Migration] Found {foundStateMachines.Count} StateMachine(s)");
|
||||
}
|
||||
|
||||
private void PingObject(StateMachineInfo info)
|
||||
{
|
||||
if (info.isPrefab)
|
||||
{
|
||||
// Load and ping the prefab asset
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(info.assetPath);
|
||||
if (prefab != null)
|
||||
{
|
||||
EditorGUIUtility.PingObject(prefab);
|
||||
Selection.activeObject = prefab;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For scene objects, we need to open the scene if it's not already open
|
||||
var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
|
||||
|
||||
if (currentScene.path != info.assetPath)
|
||||
{
|
||||
// Scene is not currently open
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Open Scene?",
|
||||
$"The object is in scene:\n{info.assetPath}\n\nDo you want to open this scene?",
|
||||
"Yes, Open Scene",
|
||||
"Cancel"))
|
||||
{
|
||||
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
|
||||
{
|
||||
EditorSceneManager.OpenScene(info.assetPath, OpenSceneMode.Single);
|
||||
|
||||
// Find the object in the newly opened scene
|
||||
if (info.gameObject != null)
|
||||
{
|
||||
// The gameObject reference might be stale, find by path
|
||||
GameObject obj = GameObject.Find(info.gameObject.name);
|
||||
if (obj != null)
|
||||
{
|
||||
Selection.activeGameObject = obj;
|
||||
EditorGUIUtility.PingObject(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scene is already open
|
||||
if (info.gameObject != null)
|
||||
{
|
||||
Selection.activeGameObject = info.gameObject;
|
||||
EditorGUIUtility.PingObject(info.gameObject);
|
||||
// Also scroll to it in hierarchy
|
||||
EditorApplication.ExecuteMenuItem("Window/General/Hierarchy");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateAll()
|
||||
{
|
||||
int migratedCount = 0;
|
||||
int errorCount = 0;
|
||||
|
||||
foreach (var info in foundStateMachines)
|
||||
{
|
||||
if (!info.isAlreadySaveable)
|
||||
{
|
||||
if (MigrateSingle(info))
|
||||
{
|
||||
migratedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
EditorUtility.DisplayDialog(
|
||||
"Migration Complete",
|
||||
$"Migrated: {migratedCount}\nErrors: {errorCount}",
|
||||
"OK"
|
||||
);
|
||||
|
||||
// Refresh list to show green highlighting for newly migrated items
|
||||
ScanProject();
|
||||
}
|
||||
|
||||
private bool MigrateSingle(StateMachineInfo info)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (info.isPrefab)
|
||||
{
|
||||
return MigratePrefab(info.assetPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MigrateSceneObject(info.assetPath, info.gameObject);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[StateMachine Migration] Error migrating '{info.name}': {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool MigratePrefab(string prefabPath)
|
||||
{
|
||||
GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
|
||||
|
||||
try
|
||||
{
|
||||
var components = prefabRoot.GetComponentsInChildren<Component>(true);
|
||||
int migratedInPrefab = 0;
|
||||
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component == null) continue;
|
||||
|
||||
var componentType = component.GetType();
|
||||
if (componentType.Name == "StateMachine" &&
|
||||
componentType.Namespace == "Pixelplacement" &&
|
||||
!(component is AppleMachine))
|
||||
{
|
||||
if (MigrateComponent(component.gameObject, component))
|
||||
{
|
||||
migratedInPrefab++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
|
||||
Debug.Log($"[StateMachine Migration] Migrated {migratedInPrefab} component(s) in prefab: {prefabPath}");
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(prefabRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private bool MigrateSceneObject(string scenePath, GameObject gameObject)
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
|
||||
|
||||
try
|
||||
{
|
||||
var components = gameObject.GetComponents<Component>();
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component == null) continue;
|
||||
|
||||
var componentType = component.GetType();
|
||||
if (componentType.Name == "StateMachine" &&
|
||||
componentType.Namespace == "Pixelplacement" &&
|
||||
!(component is AppleMachine))
|
||||
{
|
||||
bool success = MigrateComponent(gameObject, component);
|
||||
|
||||
if (success)
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
EditorSceneManager.SaveScene(scene);
|
||||
Debug.Log($"[StateMachine Migration] Migrated component in scene: {scenePath}");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorSceneManager.CloseScene(scene, false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool MigrateComponent(GameObject gameObject, Component oldComponent)
|
||||
{
|
||||
// Capture old component data using SerializedObject
|
||||
SerializedObject oldSO = new SerializedObject(oldComponent);
|
||||
|
||||
var defaultState = oldSO.FindProperty("defaultState");
|
||||
var verbose = oldSO.FindProperty("verbose");
|
||||
var allowReentry = oldSO.FindProperty("allowReentry");
|
||||
var returnToDefaultOnDisable = oldSO.FindProperty("returnToDefaultOnDisable");
|
||||
var onStateExited = oldSO.FindProperty("OnStateExited");
|
||||
var onStateEntered = oldSO.FindProperty("OnStateEntered");
|
||||
var onFirstStateEntered = oldSO.FindProperty("OnFirstStateEntered");
|
||||
var onFirstStateExited = oldSO.FindProperty("OnFirstStateExited");
|
||||
var onLastStateEntered = oldSO.FindProperty("OnLastStateEntered");
|
||||
var onLastStateExited = oldSO.FindProperty("OnLastStateExited");
|
||||
|
||||
// Remove old component
|
||||
Object.DestroyImmediate(oldComponent);
|
||||
|
||||
// Add new component
|
||||
var newSM = gameObject.AddComponent<AppleMachine>();
|
||||
|
||||
// Restore data using SerializedObject
|
||||
SerializedObject newSO = new SerializedObject(newSM);
|
||||
CopySerializedProperty(defaultState, newSO.FindProperty("defaultState"));
|
||||
CopySerializedProperty(verbose, newSO.FindProperty("verbose"));
|
||||
CopySerializedProperty(allowReentry, newSO.FindProperty("allowReentry"));
|
||||
CopySerializedProperty(returnToDefaultOnDisable, newSO.FindProperty("returnToDefaultOnDisable"));
|
||||
CopySerializedProperty(onStateExited, newSO.FindProperty("OnStateExited"));
|
||||
CopySerializedProperty(onStateEntered, newSO.FindProperty("OnStateEntered"));
|
||||
CopySerializedProperty(onFirstStateEntered, newSO.FindProperty("OnFirstStateEntered"));
|
||||
CopySerializedProperty(onFirstStateExited, newSO.FindProperty("OnFirstStateExited"));
|
||||
CopySerializedProperty(onLastStateEntered, newSO.FindProperty("OnLastStateEntered"));
|
||||
CopySerializedProperty(onLastStateExited, newSO.FindProperty("OnLastStateExited"));
|
||||
|
||||
// Set a custom Save ID based on GameObject name (simple and readable)
|
||||
// Users can customize this in the inspector if needed
|
||||
var customSaveIdProperty = newSO.FindProperty("customSaveId");
|
||||
if (customSaveIdProperty != null)
|
||||
{
|
||||
// Use GameObject name as the custom ID (scene name will be added automatically by GetSaveId)
|
||||
customSaveIdProperty.stringValue = gameObject.name;
|
||||
Debug.Log($"[Migration] Set custom Save ID: '{customSaveIdProperty.stringValue}' for '{gameObject.name}'");
|
||||
}
|
||||
|
||||
newSO.ApplyModifiedProperties();
|
||||
|
||||
EditorUtility.SetDirty(gameObject);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CopySerializedProperty(SerializedProperty source, SerializedProperty dest)
|
||||
{
|
||||
if (source == null || dest == null) return;
|
||||
|
||||
switch (source.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Integer:
|
||||
dest.intValue = source.intValue;
|
||||
break;
|
||||
case SerializedPropertyType.Boolean:
|
||||
dest.boolValue = source.boolValue;
|
||||
break;
|
||||
case SerializedPropertyType.Float:
|
||||
dest.floatValue = source.floatValue;
|
||||
break;
|
||||
case SerializedPropertyType.String:
|
||||
dest.stringValue = source.stringValue;
|
||||
break;
|
||||
case SerializedPropertyType.Color:
|
||||
dest.colorValue = source.colorValue;
|
||||
break;
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
dest.objectReferenceValue = source.objectReferenceValue;
|
||||
break;
|
||||
case SerializedPropertyType.LayerMask:
|
||||
dest.intValue = source.intValue;
|
||||
break;
|
||||
case SerializedPropertyType.Enum:
|
||||
dest.enumValueIndex = source.enumValueIndex;
|
||||
break;
|
||||
case SerializedPropertyType.Vector2:
|
||||
dest.vector2Value = source.vector2Value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector3:
|
||||
dest.vector3Value = source.vector3Value;
|
||||
break;
|
||||
case SerializedPropertyType.Vector4:
|
||||
dest.vector4Value = source.vector4Value;
|
||||
break;
|
||||
case SerializedPropertyType.Rect:
|
||||
dest.rectValue = source.rectValue;
|
||||
break;
|
||||
case SerializedPropertyType.ArraySize:
|
||||
dest.arraySize = source.arraySize;
|
||||
break;
|
||||
case SerializedPropertyType.Character:
|
||||
dest.intValue = source.intValue;
|
||||
break;
|
||||
case SerializedPropertyType.AnimationCurve:
|
||||
dest.animationCurveValue = source.animationCurveValue;
|
||||
break;
|
||||
case SerializedPropertyType.Bounds:
|
||||
dest.boundsValue = source.boundsValue;
|
||||
break;
|
||||
case SerializedPropertyType.Quaternion:
|
||||
dest.quaternionValue = source.quaternionValue;
|
||||
break;
|
||||
case SerializedPropertyType.Generic:
|
||||
// Handle UnityEvent and other generic types by copying children
|
||||
CopyGenericProperty(source, dest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyGenericProperty(SerializedProperty source, SerializedProperty dest)
|
||||
{
|
||||
// For arrays and lists
|
||||
if (source.isArray)
|
||||
{
|
||||
dest.arraySize = source.arraySize;
|
||||
for (int i = 0; i < source.arraySize; i++)
|
||||
{
|
||||
CopySerializedProperty(source.GetArrayElementAtIndex(i), dest.GetArrayElementAtIndex(i));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For complex objects, copy all children
|
||||
SerializedProperty sourceIterator = source.Copy();
|
||||
SerializedProperty destIterator = dest.Copy();
|
||||
|
||||
bool enterChildren = true;
|
||||
while (sourceIterator.Next(enterChildren))
|
||||
{
|
||||
// Only process immediate children
|
||||
if (!sourceIterator.propertyPath.StartsWith(source.propertyPath + "."))
|
||||
break;
|
||||
|
||||
enterChildren = false;
|
||||
|
||||
// Find corresponding property in destination
|
||||
string relativePath = sourceIterator.propertyPath.Substring(source.propertyPath.Length + 1);
|
||||
SerializedProperty destChild = dest.FindPropertyRelative(relativePath);
|
||||
|
||||
if (destChild != null)
|
||||
{
|
||||
CopySerializedProperty(sourceIterator, destChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StateMachineInfo
|
||||
{
|
||||
public string name;
|
||||
public string path;
|
||||
public bool isPrefab;
|
||||
public bool isAlreadySaveable;
|
||||
public string assetPath;
|
||||
public GameObject gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class TransformExtensions
|
||||
{
|
||||
public static string GetHierarchyPath(this Transform transform)
|
||||
{
|
||||
string path = transform.name;
|
||||
while (transform.parent != null)
|
||||
{
|
||||
transform = transform.parent;
|
||||
path = transform.name + "/" + path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Editor/StateMachineMigrationTool.cs.meta
Normal file
3
Assets/Editor/StateMachineMigrationTool.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13897a2afcff4d21a3eb20fe5092bf7a
|
||||
timeCreated: 1762121174
|
||||
@@ -8,7 +8,7 @@ namespace Editor
|
||||
public class ItemPrefabEditorWindow : EditorWindow
|
||||
{
|
||||
private GameObject _selectedGameObject;
|
||||
private Interactable _interactable;
|
||||
private InteractableBase _interactable;
|
||||
private PickupItemData _pickupData;
|
||||
private PuzzleStepSO _objectiveData;
|
||||
private UnityEditor.Editor _soEditor;
|
||||
@@ -42,17 +42,17 @@ namespace Editor
|
||||
if (Selection.activeGameObject != null)
|
||||
{
|
||||
_selectedGameObject = Selection.activeGameObject;
|
||||
_interactable = _selectedGameObject.GetComponent<Interactable>();
|
||||
_interactable = _selectedGameObject.GetComponent<InteractableBase>();
|
||||
}
|
||||
else if (Selection.activeObject is GameObject go)
|
||||
{
|
||||
_selectedGameObject = go;
|
||||
_interactable = go.GetComponent<Interactable>();
|
||||
_interactable = go.GetComponent<InteractableBase>();
|
||||
}
|
||||
|
||||
if (_selectedGameObject == null || _interactable == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Select a GameObject or prefab with an Interactable component to edit.", MessageType.Info);
|
||||
EditorGUILayout.HelpBox("Select a GameObject or prefab with an InteractableBase component to edit.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
using Interactions;
|
||||
@@ -124,7 +124,7 @@ namespace Editor
|
||||
private void CreatePrefab()
|
||||
{
|
||||
var go = new GameObject(_prefabName);
|
||||
go.AddComponent<Interactable>();
|
||||
// Note: No need to add InteractableBase separately - Pickup and ItemSlot inherit from it
|
||||
go.AddComponent<BoxCollider>();
|
||||
int interactableLayer = LayerMask.NameToLayer("Interactable");
|
||||
if (interactableLayer != -1)
|
||||
|
||||
Reference in New Issue
Block a user