### 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
576 lines
23 KiB
C#
576 lines
23 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
|