28 KiB
Interactables Save/Load Integration Plan
Overview
This document outlines the complete implementation plan for integrating the Interactable system (InteractableBase and all child classes) into the existing save/load system using the ISaveParticipant pattern.
Current State Analysis
Interactable Hierarchy
InteractableBase (abstract)
├── Pickup
│ └── ItemSlot (extends Pickup)
├── OneClickInteraction
├── LevelSwitch
└── MinigameSwitch
Key Observations
- InteractableBase - Abstract base class with common interaction flow, events, cooldown system
- Pickup - Tracks
isPickedUpstate, handles item pickup/combination logic - ItemSlot - Extends Pickup, tracks slotted item state (
ItemSlotState,_currentlySlottedItemData,_currentlySlottedItemObject) - OneClickInteraction - Stateless, completes immediately (likely no save state needed)
- LevelSwitch - Appears to be stateless switch behavior (no persistent state)
- MinigameSwitch - Already partially integrated with save system (tracks unlock state in
SaveLoadData.unlockedMinigames)
State Machine Challenge
- State machines (Pixelplacement's StateMachine) are used in scenes
- Calling
ChangeState()triggersOnEnable()in target state, which can:- Start animations
- Move player characters
- Enable child GameObjects with Start() calls
- Problem: Restoring a state directly would replay all initialization logic
- Solution: Need a way to restore state without triggering initialization side effects
Implementation Strategy
Phase 1: Create Base SaveableInteractable Class ✅ COMPLETED
Goal: Establish the foundation for saving/loading interactable state
Phase 1 Completion Summary:
- ✅ Created
SaveableInteractableabstract class inheriting fromInteractableBase - ✅ Implemented
ISaveParticipantinterface with GetSaveId(), SerializeState(), RestoreState() - ✅ Added hybrid save ID generation: custom IDs or auto-generated from hierarchy path
- ✅ Implemented registration/unregistration in Start()/OnDestroy()
- ✅ Added
IsRestoringFromSaveprotected flag for child classes - ✅ Created abstract methods
GetSerializableState()andApplySerializableState()for children - ✅ Added
InteractableBaseSaveDatastructure with position, rotation, and active state - ✅ Included editor helper methods (Log Save ID, Test Serialize/Deserialize)
- ✅ No compilation errors, only minor style warnings
Files Created:
Assets/Scripts/Interactions/SaveableInteractable.cs
Phase 2: Migrate Pickup to SaveableInteractable ✅ COMPLETED
Goal: Make Pickup save/restore its picked-up state and world position
Phase 2 Completion Summary:
- ✅ Changed
Pickupto extendSaveableInteractableinstead ofInteractableBase - ✅ Created
PickupSaveDatastructure with: isPickedUp, worldPosition, worldRotation, isActive - ✅ Updated Start() to call base.Start() and skip ItemManager registration if already picked up
- ✅ Updated OnDestroy() to call base.OnDestroy() for save system unregistration
- ✅ Implemented
GetSerializableState()to capture current pickup state - ✅ Implemented
ApplySerializableState()to restore state without triggering events - ✅ Added world position/rotation saving for items that haven't been picked up (user requirement)
- ✅ Hide GameObject if picked up, restore position/rotation if not picked up
- ✅ No OnItemPickedUp events fired during restoration (prevents duplicate logic)
- ✅ No compilation errors, only style warnings
Files Modified:
Assets/Scripts/Interactions/Pickup.cs
Phase 3: Migrate ItemSlot to SaveableInteractable ✅ COMPLETED
Goal: Make ItemSlot save/restore slotted item state
State to Save:
_currentState(ItemSlotState enum) - Current slot validation state_currentlySlottedItemData(PickupItemData reference) - Which item is slotted- Base Pickup state (isPickedUp) - Inherited from Pickup
Serialization Strategy:
[System.Serializable]
public class ItemSlotSaveData
{
public bool isPickedUp; // From Pickup base
public ItemSlotState slotState;
public string slottedItemDataGuid; // Reference to PickupItemData asset
}
Restoration Logic:
- Restore base Pickup state (isPickedUp) Phase 3 Completion Summary:
- ✅ Created
ItemSlotSaveDatastructure with: pickupData, slotState, slottedItemSaveId, slottedItemDataAssetPath - ✅ Updated Start() to call base.Start() and additionally register as ItemSlot
- ✅ Updated OnDestroy() to call base.OnDestroy() and unregister from ItemSlot manager
- ✅ Implemented
GetSerializableState()to capture slot state and slotted item references - ✅ Implemented
ApplySerializableState()to restore base pickup state + slot state - ✅ Created
ApplySlottedItemState()method - unified slotting logic with triggerEvents parameter (post-refactor) - ✅ Created
RestoreSlottedItem()method - finds slotted item by save ID and restores it - ✅ SlotItem() refactored - now a clean wrapper around ApplySlottedItemState (post-refactor)
- ✅ Reuses base Pickup serialization through inheritance (code reuse achieved!)
- ✅ No compilation errors, only style warnings
Code Reuse Pattern Identified:
- ItemSlot calls
base.GetSerializableState()to get PickupSaveData - ItemSlot calls
base.ApplySerializableState()to restore pickup state first - ApplySlottedItemState() handles both gameplay and restoration with a single code path
- This pattern can be used by future child classes (inheritance-based state composition)
Files Modified:
Assets/Scripts/Interactions/ItemSlot.csAssets/Scripts/Core/ItemManager.cs(added GetAllPickups/GetAllItemSlots/FindPickupBySaveId methods)
Phase 4: Handle Stateless Interactables ✅ COMPLETED
Goal: Determine which interactables need saving and migrate MinigameSwitch to participant pattern
Phase 4 Completion Summary:
- ✅ OneClickInteraction: Confirmed stateless, kept inheriting from InteractableBase
- ✅ LevelSwitch: Confirmed stateless, kept inheriting from InteractableBase
- ✅ MinigameSwitch: Successfully migrated to SaveableInteractable
- Created
MinigameSwitchSaveDatastructure with isUnlocked flag - Changed inheritance from InteractableBase to SaveableInteractable
- Removed old direct SaveLoadManager.currentSaveData access pattern
- Removed manual event subscription to OnLoadCompleted
- Added
_isUnlockedprivate field to track state - Implemented
GetSerializableState()andApplySerializableState() - Updated Start() to check IsRestoringFromSave flag and set initial active state
- Updated HandleAllPuzzlesComplete() to set unlock flag (save happens automatically)
- GameObject.activeSelf now managed by save/load system
- Created
- ✅ No compilation errors, only style warnings
Migration Benefit:
- MinigameSwitch now uses the same pattern as all other saveable interactables
- No special case code needed in SaveLoadManager
- Cleaner, more maintainable architecture
Files Modified:
Assets/Scripts/Levels/MinigameSwitch.cs
Code Quality Improvements (Post Phase 1-4)
Refactoring Round 1 ✅ COMPLETED
Issues Identified:
- ItemSlot Code Duplication - SlotItem() and SlotItemSilent() had significant duplicate logic
- Poor Method Placement - FindPickupBySaveId() was in ItemSlot but should be in ItemManager
- Bootstrap Timing Issue - SaveableInteractable didn't handle save data loading before/after registration
Improvements Implemented:
1. ItemSlot Refactoring
- ✅ Extracted common logic into
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents, clearFollowerHeldItem) - ✅ Eliminated code duplication - SlotItem() and restoration both use the same core logic
- ✅ Added triggerEvents parameter - single method handles both interactive and silent slotting
- ✅ Simplified public API -
SlotItem()is now a thin wrapper aroundApplySlottedItemState()
Before:
SlotItem() // 60 lines of logic
SlotItemSilent() // 15 lines of duplicated logic
After:
ApplySlottedItemState(triggerEvents) // 85 lines - single source of truth
SlotItem() // 3 lines - wrapper for gameplay
RestoreSlottedItem() // uses ApplySlottedItemState with triggerEvents=false
2. ItemManager Enhancement
- ✅ Moved FindPickupBySaveId() from ItemSlot to ItemManager (proper separation of concerns)
- ✅ Centralized item lookup - ItemManager is now the single source for finding items by save ID
- ✅ Cleaner architecture - ItemSlot no longer needs to know about scene searching
New ItemManager API:
public GameObject FindPickupBySaveId(string saveId)
3. SaveableInteractable Bootstrap Fix
- ✅ Added hasRestoredState flag - tracks whether state has been restored
- ✅ Subscribes to OnLoadCompleted - handles save data loading after registration
- ✅ Unsubscribes properly - prevents memory leaks
- ✅ Mirrors MinigameSwitch pattern - uses same pattern that was working before
Bootstrap Flow:
- SaveableInteractable registers in Start()
- Checks if save data is already loaded
- If not loaded yet, subscribes to OnLoadCompleted event
- When load completes, SaveLoadManager calls RestoreState() automatically
- Unsubscribes from event to prevent duplicate restoration
Files Modified:
Assets/Scripts/Interactions/ItemSlot.cs(refactored slot logic)Assets/Scripts/Core/ItemManager.cs(added FindPickupBySaveId)Assets/Scripts/Interactions/SaveableInteractable.cs(fixed bootstrap timing)
Refactoring Round 2 - Critical Fixes ✅ COMPLETED
Issue 1: Item Swap Not Resetting Pickup State
Problem: When picking up Item B while holding Item A, Item A was dropped but remained marked as isPickedUp = true, causing it to be hidden on load.
Solution:
- ✅ Added
ResetPickupState()method to Pickup class - ✅ Changed
isPickedUpsetter fromprivatetointernal - ✅ Updated
FollowerController.DropItem()to callResetPickupState() - ✅ Fixed combination edge case: base items marked as picked up before destruction
Files Modified:
Assets/Scripts/Interactions/Pickup.csAssets/Scripts/Movement/FollowerController.cs
Issue 2: Serialization Field Name Conflict
Problem: Unity error - _isActive field name serialized multiple times in class hierarchy (InteractableBase → MinigameSwitch/LevelSwitch).
Solution:
- ✅ Renamed child class fields to be more specific:
_isActive→switchActive - ✅ Updated all references in MinigameSwitch (4 occurrences)
- ✅ Updated all references in LevelSwitch (5 occurrences)
Files Modified:
Assets/Scripts/Levels/MinigameSwitch.csAssets/Scripts/Levels/LevelSwitch.cs
Issue 3: State Machine Initialization Order - CRITICAL 🔥 FIXED
Problem: Objects controlled by state machines start disabled. When they're enabled mid-gameplay:
- Object becomes active
Start()runs → registers with SaveLoadManager- SaveLoadManager immediately calls
RestoreState() - Object teleports to saved position during gameplay ❌
Root Cause: Disabled GameObjects don't run Awake()/Start() until enabled. Late registration triggers immediate restoration.
Solution - SaveLoadManager Discovery + Participant Self-Tracking:
Key Principles:
- ✅ SaveableInteractable keeps normal lifecycle (Start/Awake/OnDestroy) unchanged
- ✅ SaveLoadManager actively discovers INACTIVE SaveableInteractables on scene load
- ✅ Each participant tracks if it's been restored via
HasBeenRestoredproperty - ✅ Prevent double-restoration using participant's own state (no redundant tracking)
Implementation:
-
ISaveParticipant Interface - Added HasBeenRestored property:
bool HasBeenRestored { get; } -
SaveableInteractable - Exposed existing flag:
private bool hasRestoredState; public bool HasBeenRestored => hasRestoredState; -
SaveLoadManager.OnSceneLoadCompleted() - Active Discovery:
var inactiveSaveables = FindObjectsByType(typeof(SaveableInteractable), FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var obj in inactiveSaveables) { var saveable = obj as SaveableInteractable; if (saveable != null && !saveable.gameObject.activeInHierarchy) { RegisterParticipant(saveable); // Register inactive objects } } -
RegisterParticipant() - Check Participant's Own State:
if (IsSaveDataLoaded && !IsRestoringState && !participant.HasBeenRestored) { RestoreParticipantState(participant); // Only if not already restored }
Flow:
Initial Scene Load (with inactive objects):
- Scene loads
- Active objects: Start() → RegisterParticipant() → RestoreState() → hasRestoredState = true
- OnSceneLoadCompleted() fires
- FindObjectsByType(includeInactive) discovers inactive SaveableInteractables
- RegisterParticipant(inactive object) → checks HasBeenRestored → false → RestoreState() → hasRestoredState = true
- All objects restored and tracked ✅
Mid-Gameplay Object Enablement:
- State machine enables GameObject
- Awake() runs → Start() runs → RegisterParticipant()
- Check:
participant.HasBeenRestored→ TRUE - Skip RestoreParticipantState() ✅
- NO TELEPORTATION ✅
Files Modified:
Assets/Scripts/Core/SaveLoad/ISaveParticipant.cs(added HasBeenRestored property)Assets/Scripts/Interactions/SaveableInteractable.cs(exposed hasRestoredState)Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs(discovery + participant check)Assets/Scripts/Data/CardSystem/CardSystemManager.cs(implemented HasBeenRestored)
Phase 5: State Machine Integration Pattern
Goal: Provide a safe way for state machines to react to interactable restoration without replaying initialization
Problem Analysis:
- State machines like
GardenerBehaviour.stateSwitch(string stateName)are called via UnityEvents - Current flow: Pickup interaction → Event → stateSwitch() → ChangeState() → OnEnable() → Animations/Movement
- During load: We want to restore the state WITHOUT triggering animations/movement
Solution Options:
Option A: State Restoration Flag (Recommended)
-
Add
IsRestoringFromSavestatic/singleton flag to SaveLoadManager -
State machines check this flag in OnEnable():
void OnEnable() { if (SaveLoadManager.Instance?.IsRestoringState == true) { // Silent restoration - set state variables only return; } // Normal initialization with animations/movement } -
SaveableInteractable sets flag before calling RestoreState(), clears after
Advantages:
- Minimal changes to existing state machine code
- Clear separation of concerns
- Easy to understand and debug
Disadvantages:
- Requires modifying every State class
- Global flag could cause issues if restoration is async
Option B: Separate Restoration Methods
-
State classes implement two entry points:
OnEnable()- Normal initialization with side effectsRestoreState()- Silent state restoration without side effects
-
Add a
StateRestorationHelperthat can restore states silently:public static class StateRestorationHelper { public static void RestoreStateSilently(StateMachine sm, string stateName) { // Manually set active state without calling OnEnable } }
Advantages:
- More explicit, less global state
- States control their own restoration logic
Disadvantages:
- More code duplication
- Harder to implement with existing StateMachine plugin
Option C: Save State Machine State Separately
- Don't tie state machine state to interactable state
- Create separate
SaveableStateMachinecomponent - State machines save their own current state
- On load, restore state machine state independently
Advantages:
- Clean separation of concerns
- State machines are self-contained
Disadvantages:
- More complex save data structure
- Need to coordinate restoration order
Recommendation: Use Option A with a restoration flag. It's the least invasive and most compatible with the existing Pixelplacement StateMachine plugin.
Implementation Plan:
-
Add
IsRestoringStateflag to SaveLoadManager (already exists!) -
Create helper component
SaveableStatethat wraps State classes:public class SaveableState : State { protected virtual void OnEnableBase() { if (SaveLoadManager.Instance?.IsRestoringState == true) { OnRestoreState(); return; } OnNormalEnable(); } protected virtual void OnNormalEnable() { } protected virtual void OnRestoreState() { } } -
Migrate existing states to inherit from SaveableState
-
Move initialization logic from OnEnable to OnNormalEnable
-
Add minimal restoration logic to OnRestoreState
Files to Create:
Assets/Scripts/StateMachines/SaveableState.cs
Files to Modify (examples):
Assets/Scripts/StateMachines/Quarry/AnneLise/TakePhotoState.cs- Any other State classes that react to interactable events
Phase 6: Unique ID Generation Strategy
Goal: Ensure every saveable interactable has a persistent, unique ID across sessions
Challenge:
- Scene instance IDs change between sessions
- Need deterministic IDs that survive scene reloads
- Manual GUID assignment is error-prone
Solution: Hybrid Approach
For Prefab Instances:
- Use prefab path + scene path + sibling index
- Example:
"Quarry/PickupApple_0"(first apple in scene) - Stable as long as prefab hierarchy doesn't change
For Unique Scene Objects:
- Add
[SerializeField] private string customSaveId - Editor tool validates uniqueness within scene
- Example:
"Quarry/UniquePickup_GoldenApple"
Implementation:
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
{
[SerializeField] private string customSaveId = "";
public string GetSaveId()
{
if (!string.IsNullOrEmpty(customSaveId))
{
return $"{GetSceneName()}/{customSaveId}";
}
// Auto-generate from hierarchy
string path = GetHierarchyPath();
return $"{GetSceneName()}/{path}";
}
private string GetHierarchyPath()
{
// Build path from scene root to this object
// Include sibling index for uniqueness
}
}
Editor Tool:
- Create validation window that scans scene for duplicate IDs
- Auto-generate IDs for objects missing them
- Warn if hierarchy changes break ID stability
Files to Create:
Assets/Editor/SaveIdValidator.cs
Phase 7: ItemManager Integration
Goal: Handle cases where slotted items need to be spawned/found during restoration
Current ItemManager Functionality:
- Registers/unregisters Pickups and ItemSlots
- No explicit save/load support
Required Additions:
-
Item Lookup by Save ID:
public Pickup GetPickupBySaveId(string saveId) -
Item Reference Serialization:
[System.Serializable] public class ItemReference { public string saveId; // For scene pickups public string prefabPath; // For prefab items public string itemDataGuid; // For PickupItemData } -
Item Spawning for Restoration:
public GameObject SpawnItemForLoad(ItemReference itemRef) { // Check if item exists in scene first // If not, instantiate from prefab // Apply item data // Return GameObject }
ItemSlot Restoration Flow:
- ItemSlot restores state, finds it had a slotted item
- Calls
ItemManager.GetPickupBySaveId(slottedItemSaveId) - If found: Use existing GameObject
- If not found: Call
ItemManager.SpawnItemForLoad(itemReference) - Call
SlotItem()with restored GameObject
Edge Cases:
- Item was picked up and slotted elsewhere: Use save ID to track
- Item was combined before slotting: May not exist anymore (store flag)
- Item was from player inventory: Need FollowerController integration
Files to Modify:
Assets/Scripts/Core/ItemManager.cs
Phase 8: FollowerController Integration (Optional)
Goal: Save/restore items currently held by the follower character
State to Save:
- Currently held item reference
- Held item position/rotation
Decision:
- Phase 1: Skip this, assume player starts levels with empty inventory
- Future Enhancement: Add when inventory persistence is needed
Reasoning:
- Most levels are self-contained
- Overworld persistence can be added later
- Reduces initial complexity
Implementation Order & Dependencies
Phase 1: Foundation (No Dependencies) ✅
- Create SaveableInteractable base class
- Implement ISaveParticipant
- Add save ID generation
- Test with dummy data
Deliverable: SaveableInteractable.cs compiles and can register/unregister
Phase 2: Pickup Migration (Depends on Phase 1)
- Modify Pickup to extend SaveableInteractable
- Implement PickupSaveData serialization
- Test pickup state save/load
- Verify picked-up items stay hidden after load
Deliverable: Can save/load a scene with pickups, picked-up items remain picked up
Phase 3: ItemSlot Migration (Depends on Phases 1, 2, 7)
- Modify ItemSlot to extend SaveableInteractable
- Implement ItemSlotSaveData serialization
- Integrate with ItemManager for item references
- Test slotted items restore correctly
Deliverable: Can save/load a scene with item slots, slotted items remain slotted
Phase 4: Stateless Interactables (Depends on Phase 1)
- Migrate MinigameSwitch to SaveableInteractable
- Remove direct SaveLoadData access
- Test minigame unlock persistence
Deliverable: MinigameSwitch uses participant pattern instead of direct access
Phase 5: State Machine Integration (Independent)
- Create SaveableState base class
- Migrate example states (TakePhotoState, etc.)
- Test state restoration without side effects
- Document pattern for future states
Deliverable: State machines can restore without replaying animations
Phase 6: Save ID System (Depends on Phase 1)
- Implement GetSaveId() with hierarchy path
- Add custom save ID field
- Create editor validation tool
- Test ID stability across sessions
Deliverable: All saveable interactables have stable, unique IDs
Phase 7: ItemManager Integration (Independent, but needed for Phase 3)
- Add item lookup methods to ItemManager
- Implement ItemReference serialization
- Add item spawning for restoration
- Test with ItemSlot restoration
Deliverable: ItemManager can find/spawn items by reference during load
Phase 8: Testing & Polish (Depends on all previous phases)
- Test full save/load cycle in each level
- Test edge cases (combined items, missing prefabs, etc.)
- Add error handling and logging
- Update documentation
Deliverable: Robust save/load system for all interactables
Data Structures
SaveableInteractable Base State
[System.Serializable]
public class InteractableBaseSaveData
{
public bool isActive; // GameObject.activeSelf
public float remainingCooldown; // If cooldown system is used
}
Pickup State
[System.Serializable]
public class PickupSaveData
{
public InteractableBaseSaveData baseData;
public bool isPickedUp;
}
ItemSlot State
[System.Serializable]
public class ItemSlotSaveData
{
public PickupSaveData pickupData; // Inherited state
public ItemSlotState slotState;
public ItemReference slottedItem; // Null if empty
}
[System.Serializable]
public class ItemReference
{
public string saveId; // For scene objects
public string prefabPath; // For prefab spawning
public string itemDataGuid; // PickupItemData reference
}
MinigameSwitch State
[System.Serializable]
public class MinigameSwitchSaveData
{
public InteractableBaseSaveData baseData;
public bool isUnlocked;
}
Save/Load Flow
Save Flow
- SaveLoadManager iterates all registered participants
- Calls
GetSaveId()on each SaveableInteractable - Calls
SerializeState()on each - SaveableInteractable:
- Calls virtual
GetSerializableState()(child override) - Serializes to JSON
- Returns string
- Calls virtual
- SaveLoadManager stores in
participantStatesdictionary - Writes entire SaveLoadData to disk
Load Flow
- SaveLoadManager reads SaveLoadData from disk
- Sets
IsRestoringState = true - Scene loads (interactables register during Start())
- On registration, SaveLoadManager checks for existing state
- If state exists, immediately calls
RestoreState(serializedData) - SaveableInteractable:
- Deserializes JSON
- Calls virtual
ApplySerializableState(stateData)(child override) - Child applies state WITHOUT triggering events/initialization
- SaveLoadManager sets
IsRestoringState = false
State Machine Flow (During Load)
- Interactable restores, determines it should trigger state "Scared"
- Instead of firing UnityEvent, directly calls StateMachine.ChangeState("Scared")
- StateMachine activates "Scared" state GameObject
- "Scared" State's OnEnable() checks
SaveLoadManager.IsRestoringState - If true: Sets internal variables only (no animations/movement)
- If false: Normal initialization
Error Handling
Missing Item References
- Problem: ItemSlot has slotted item, but item no longer exists
- Solution: Log warning, set slot to empty state
Duplicate Save IDs
- Problem: Two interactables generate same save ID
- Solution: Editor validation tool catches this, manual fix required
Corrupted Save Data
- Problem: JSON deserialization fails
- Solution: Log error, use default state, continue loading
State Machine Mismatch
- Problem: Saved state references state that no longer exists
- Solution: Log warning, set to initial state
Testing Strategy
Unit Tests
- SaveableInteractable registration/unregistration
- Save ID generation uniqueness
- Serialization/deserialization round-trip
Integration Tests
- Save scene with various interactable states
- Load scene and verify all states restored
- Test item slot with slotted items
- Test state machine restoration
- Test edge cases (missing items, corrupted data)
Playthrough Tests
- Play through each level
- Save at various points
- Load and verify game state is correct
- Test all puzzle solutions still work
Migration Checklist
- Phase 1: Create SaveableInteractable base class
- Phase 2: Migrate Pickup to SaveableInteractable
- Phase 3: Migrate ItemSlot to SaveableInteractable
- Phase 4: Migrate MinigameSwitch to SaveableInteractable
- Phase 5: Create SaveableState pattern for state machines
- Phase 6: Implement save ID validation system
- Phase 7: Extend ItemManager with lookup/spawn functionality
- Phase 8: Full integration testing and polish
Future Enhancements
- Inventory Persistence: Save FollowerController held items
- Dynamic Object Spawning: Handle runtime-spawned interactables
- State Machine Auto-Save: Automatically save StateMachine current state
- Incremental Saves: Save only changed interactables (delta saves)
- Cloud Sync: Sync save data across devices
- Save Slots: Multiple save files per player
Notes & Considerations
Why Not Auto-Discover Interactables?
- Performance: Scanning scenes is expensive
- Determinism: Registration order affects save ID generation
- Control: Objects control their own lifecycle
Why Scene-Based Save IDs?
- Isolation: Each scene's interactables are independent
- Clarity: Easy to debug (see which scene an ID belongs to)
- Flexibility: Can reload individual scenes without affecting others
Why Not Save Everything?
- Stateless interactables (OneClickInteraction, LevelSwitch) don't need persistence
- Reduces save file size
- Faster save/load times
- Less complexity
State Machine Plugin Limitations
- Pixelplacement's StateMachine doesn't support silent state changes
- OnEnable is always called when state activates
- Need wrapper pattern (SaveableState) to intercept
- Alternative: Fork plugin and add restoration mode
Document Version
- Version: 1.0
- Date: November 2, 2025
- Author: GitHub Copilot
- Status: Ready for Implementation