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: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/44
2025-11-03 10:12:51 +00:00
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 ;
2025-11-07 09:32:43 +01:00
[MenuItem("AppleHills/Developer/Remove InteractableBase Components")]
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: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/44
2025-11-03 10:12:51 +00:00
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 ;
}
}
}