818 lines
28 KiB
Markdown
818 lines
28 KiB
Markdown
|
|
# 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
|
||
|
|
1. **InteractableBase** - Abstract base class with common interaction flow, events, cooldown system
|
||
|
|
2. **Pickup** - Tracks `isPickedUp` state, handles item pickup/combination logic
|
||
|
|
3. **ItemSlot** - Extends Pickup, tracks slotted item state (`ItemSlotState`, `_currentlySlottedItemData`, `_currentlySlottedItemObject`)
|
||
|
|
4. **OneClickInteraction** - Stateless, completes immediately (likely no save state needed)
|
||
|
|
5. **LevelSwitch** - Appears to be stateless switch behavior (no persistent state)
|
||
|
|
6. **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()` triggers `OnEnable()` 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 `SaveableInteractable` abstract class inheriting from `InteractableBase`
|
||
|
|
- ✅ Implemented `ISaveParticipant` interface with GetSaveId(), SerializeState(), RestoreState()
|
||
|
|
- ✅ Added hybrid save ID generation: custom IDs or auto-generated from hierarchy path
|
||
|
|
- ✅ Implemented registration/unregistration in Start()/OnDestroy()
|
||
|
|
- ✅ Added `IsRestoringFromSave` protected flag for child classes
|
||
|
|
- ✅ Created abstract methods `GetSerializableState()` and `ApplySerializableState()` for children
|
||
|
|
- ✅ Added `InteractableBaseSaveData` structure 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 `Pickup` to extend `SaveableInteractable` instead of `InteractableBase`
|
||
|
|
- ✅ Created `PickupSaveData` structure 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**:
|
||
|
|
```csharp
|
||
|
|
[System.Serializable]
|
||
|
|
public class ItemSlotSaveData
|
||
|
|
{
|
||
|
|
public bool isPickedUp; // From Pickup base
|
||
|
|
public ItemSlotState slotState;
|
||
|
|
public string slottedItemDataGuid; // Reference to PickupItemData asset
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Restoration Logic**:
|
||
|
|
1. Restore base Pickup state (isPickedUp)
|
||
|
|
**Phase 3 Completion Summary:**
|
||
|
|
- ✅ Created `ItemSlotSaveData` structure 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.cs`
|
||
|
|
- `Assets/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 `MinigameSwitchSaveData` structure with isUnlocked flag
|
||
|
|
- Changed inheritance from InteractableBase to SaveableInteractable
|
||
|
|
- Removed old direct SaveLoadManager.currentSaveData access pattern
|
||
|
|
- Removed manual event subscription to OnLoadCompleted
|
||
|
|
- Added `_isUnlocked` private field to track state
|
||
|
|
- Implemented `GetSerializableState()` and `ApplySerializableState()`
|
||
|
|
- 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
|
||
|
|
- ✅ 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:**
|
||
|
|
1. **ItemSlot Code Duplication** - SlotItem() and SlotItemSilent() had significant duplicate logic
|
||
|
|
2. **Poor Method Placement** - FindPickupBySaveId() was in ItemSlot but should be in ItemManager
|
||
|
|
3. **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 around `ApplySlottedItemState()`
|
||
|
|
|
||
|
|
**Before:**
|
||
|
|
```csharp
|
||
|
|
SlotItem() // 60 lines of logic
|
||
|
|
SlotItemSilent() // 15 lines of duplicated logic
|
||
|
|
```
|
||
|
|
|
||
|
|
**After:**
|
||
|
|
```csharp
|
||
|
|
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:**
|
||
|
|
```csharp
|
||
|
|
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:**
|
||
|
|
1. SaveableInteractable registers in Start()
|
||
|
|
2. Checks if save data is already loaded
|
||
|
|
3. If not loaded yet, subscribes to OnLoadCompleted event
|
||
|
|
4. When load completes, SaveLoadManager calls RestoreState() automatically
|
||
|
|
5. 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 `isPickedUp` setter from `private` to `internal`
|
||
|
|
- ✅ Updated `FollowerController.DropItem()` to call `ResetPickupState()`
|
||
|
|
- ✅ Fixed combination edge case: base items marked as picked up before destruction
|
||
|
|
|
||
|
|
**Files Modified:**
|
||
|
|
- `Assets/Scripts/Interactions/Pickup.cs`
|
||
|
|
- `Assets/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.cs`
|
||
|
|
- `Assets/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:
|
||
|
|
1. Object becomes active
|
||
|
|
2. `Start()` runs → registers with SaveLoadManager
|
||
|
|
3. SaveLoadManager immediately calls `RestoreState()`
|
||
|
|
4. 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 `HasBeenRestored` property
|
||
|
|
- ✅ Prevent double-restoration using participant's own state (no redundant tracking)
|
||
|
|
|
||
|
|
**Implementation:**
|
||
|
|
|
||
|
|
1. **ISaveParticipant Interface** - Added HasBeenRestored property:
|
||
|
|
```csharp
|
||
|
|
bool HasBeenRestored { get; }
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **SaveableInteractable** - Exposed existing flag:
|
||
|
|
```csharp
|
||
|
|
private bool hasRestoredState;
|
||
|
|
public bool HasBeenRestored => hasRestoredState;
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **SaveLoadManager.OnSceneLoadCompleted()** - Active Discovery:
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **RegisterParticipant()** - Check Participant's Own State:
|
||
|
|
```csharp
|
||
|
|
if (IsSaveDataLoaded && !IsRestoringState && !participant.HasBeenRestored) {
|
||
|
|
RestoreParticipantState(participant); // Only if not already restored
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Flow:**
|
||
|
|
|
||
|
|
**Initial Scene Load (with inactive objects):**
|
||
|
|
1. Scene loads
|
||
|
|
2. Active objects: Start() → RegisterParticipant() → RestoreState() → hasRestoredState = true
|
||
|
|
3. OnSceneLoadCompleted() fires
|
||
|
|
4. FindObjectsByType(includeInactive) discovers inactive SaveableInteractables
|
||
|
|
5. RegisterParticipant(inactive object) → checks HasBeenRestored → false → RestoreState() → hasRestoredState = true
|
||
|
|
6. All objects restored and tracked ✅
|
||
|
|
|
||
|
|
**Mid-Gameplay Object Enablement:**
|
||
|
|
1. State machine enables GameObject
|
||
|
|
2. Awake() runs → Start() runs → RegisterParticipant()
|
||
|
|
3. Check: `participant.HasBeenRestored` → TRUE
|
||
|
|
4. Skip RestoreParticipantState() ✅
|
||
|
|
5. **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)
|
||
|
|
1. Add `IsRestoringFromSave` static/singleton flag to SaveLoadManager
|
||
|
|
2. State machines check this flag in OnEnable():
|
||
|
|
```csharp
|
||
|
|
void OnEnable()
|
||
|
|
{
|
||
|
|
if (SaveLoadManager.Instance?.IsRestoringState == true)
|
||
|
|
{
|
||
|
|
// Silent restoration - set state variables only
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Normal initialization with animations/movement
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
3. 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
|
||
|
|
1. State classes implement two entry points:
|
||
|
|
- `OnEnable()` - Normal initialization with side effects
|
||
|
|
- `RestoreState()` - Silent state restoration without side effects
|
||
|
|
|
||
|
|
2. Add a `StateRestorationHelper` that can restore states silently:
|
||
|
|
```csharp
|
||
|
|
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
|
||
|
|
1. Don't tie state machine state to interactable state
|
||
|
|
2. Create separate `SaveableStateMachine` component
|
||
|
|
3. State machines save their own current state
|
||
|
|
4. 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**:
|
||
|
|
1. Add `IsRestoringState` flag to SaveLoadManager (already exists!)
|
||
|
|
2. Create helper component `SaveableState` that wraps State classes:
|
||
|
|
```csharp
|
||
|
|
public class SaveableState : State
|
||
|
|
{
|
||
|
|
protected virtual void OnEnableBase()
|
||
|
|
{
|
||
|
|
if (SaveLoadManager.Instance?.IsRestoringState == true)
|
||
|
|
{
|
||
|
|
OnRestoreState();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
OnNormalEnable();
|
||
|
|
}
|
||
|
|
|
||
|
|
protected virtual void OnNormalEnable() { }
|
||
|
|
protected virtual void OnRestoreState() { }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
3. Migrate existing states to inherit from SaveableState
|
||
|
|
4. Move initialization logic from OnEnable to OnNormalEnable
|
||
|
|
5. 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:
|
||
|
|
```csharp
|
||
|
|
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**:
|
||
|
|
1. **Item Lookup by Save ID**:
|
||
|
|
```csharp
|
||
|
|
public Pickup GetPickupBySaveId(string saveId)
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Item Reference Serialization**:
|
||
|
|
```csharp
|
||
|
|
[System.Serializable]
|
||
|
|
public class ItemReference
|
||
|
|
{
|
||
|
|
public string saveId; // For scene pickups
|
||
|
|
public string prefabPath; // For prefab items
|
||
|
|
public string itemDataGuid; // For PickupItemData
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Item Spawning for Restoration**:
|
||
|
|
```csharp
|
||
|
|
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**:
|
||
|
|
1. ItemSlot restores state, finds it had a slotted item
|
||
|
|
2. Calls `ItemManager.GetPickupBySaveId(slottedItemSaveId)`
|
||
|
|
3. If found: Use existing GameObject
|
||
|
|
4. If not found: Call `ItemManager.SpawnItemForLoad(itemReference)`
|
||
|
|
5. 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
|
||
|
|
```csharp
|
||
|
|
[System.Serializable]
|
||
|
|
public class InteractableBaseSaveData
|
||
|
|
{
|
||
|
|
public bool isActive; // GameObject.activeSelf
|
||
|
|
public float remainingCooldown; // If cooldown system is used
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pickup State
|
||
|
|
```csharp
|
||
|
|
[System.Serializable]
|
||
|
|
public class PickupSaveData
|
||
|
|
{
|
||
|
|
public InteractableBaseSaveData baseData;
|
||
|
|
public bool isPickedUp;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### ItemSlot State
|
||
|
|
```csharp
|
||
|
|
[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
|
||
|
|
```csharp
|
||
|
|
[System.Serializable]
|
||
|
|
public class MinigameSwitchSaveData
|
||
|
|
{
|
||
|
|
public InteractableBaseSaveData baseData;
|
||
|
|
public bool isUnlocked;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Save/Load Flow
|
||
|
|
|
||
|
|
### Save Flow
|
||
|
|
1. SaveLoadManager iterates all registered participants
|
||
|
|
2. Calls `GetSaveId()` on each SaveableInteractable
|
||
|
|
3. Calls `SerializeState()` on each
|
||
|
|
4. SaveableInteractable:
|
||
|
|
- Calls virtual `GetSerializableState()` (child override)
|
||
|
|
- Serializes to JSON
|
||
|
|
- Returns string
|
||
|
|
5. SaveLoadManager stores in `participantStates` dictionary
|
||
|
|
6. Writes entire SaveLoadData to disk
|
||
|
|
|
||
|
|
### Load Flow
|
||
|
|
1. SaveLoadManager reads SaveLoadData from disk
|
||
|
|
2. Sets `IsRestoringState = true`
|
||
|
|
3. Scene loads (interactables register during Start())
|
||
|
|
4. On registration, SaveLoadManager checks for existing state
|
||
|
|
5. If state exists, immediately calls `RestoreState(serializedData)`
|
||
|
|
6. SaveableInteractable:
|
||
|
|
- Deserializes JSON
|
||
|
|
- Calls virtual `ApplySerializableState(stateData)` (child override)
|
||
|
|
- Child applies state WITHOUT triggering events/initialization
|
||
|
|
7. SaveLoadManager sets `IsRestoringState = false`
|
||
|
|
|
||
|
|
### State Machine Flow (During Load)
|
||
|
|
1. Interactable restores, determines it should trigger state "Scared"
|
||
|
|
2. Instead of firing UnityEvent, directly calls StateMachine.ChangeState("Scared")
|
||
|
|
3. StateMachine activates "Scared" state GameObject
|
||
|
|
4. "Scared" State's OnEnable() checks `SaveLoadManager.IsRestoringState`
|
||
|
|
5. If true: Sets internal variables only (no animations/movement)
|
||
|
|
6. 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
|
||
|
|
1. SaveableInteractable registration/unregistration
|
||
|
|
2. Save ID generation uniqueness
|
||
|
|
3. Serialization/deserialization round-trip
|
||
|
|
|
||
|
|
### Integration Tests
|
||
|
|
1. Save scene with various interactable states
|
||
|
|
2. Load scene and verify all states restored
|
||
|
|
3. Test item slot with slotted items
|
||
|
|
4. Test state machine restoration
|
||
|
|
5. Test edge cases (missing items, corrupted data)
|
||
|
|
|
||
|
|
### Playthrough Tests
|
||
|
|
1. Play through each level
|
||
|
|
2. Save at various points
|
||
|
|
3. Load and verify game state is correct
|
||
|
|
4. 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
|
||
|
|
|
||
|
|
1. **Inventory Persistence**: Save FollowerController held items
|
||
|
|
2. **Dynamic Object Spawning**: Handle runtime-spawned interactables
|
||
|
|
3. **State Machine Auto-Save**: Automatically save StateMachine current state
|
||
|
|
4. **Incremental Saves**: Save only changed interactables (delta saves)
|
||
|
|
5. **Cloud Sync**: Sync save data across devices
|
||
|
|
6. **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
|
||
|
|
|