Files
AppleHillsProduction/docs/state_machine_save_load_FINAL_SUMMARY.md

410 lines
10 KiB
Markdown
Raw Normal View History

# State Machine Save/Load Integration - FINAL IMPLEMENTATION
**Date:** November 2, 2025
**Status:** ✅ COMPLETE - Clean Inheritance Pattern with Zero Library Modifications
## 🎯 Final Architecture
After exploring multiple approaches (wrapper components, adapters, direct modification), we settled on the cleanest solution:
### The Solution: Dual Inheritance Pattern
```
Pixelplacement Code (UNCHANGED):
├─ StateMachine.cs (base class)
└─ State.cs (base class)
AppleHills Code:
├─ SaveableStateMachine.cs : StateMachine, ISaveParticipant
└─ SaveableState.cs : State
└─ GardenerChaseBehavior.cs : SaveableState (example)
```
**Key Principle:** We extend the library through inheritance, not modification.
---
## 📁 Files Overview
### 1. SaveableStateMachine.cs ✅
**Location:** `Assets/Scripts/Core/SaveLoad/SaveableStateMachine.cs`
**What it does:**
- Inherits from `Pixelplacement.StateMachine`
- Implements `ISaveParticipant` for save/load system
- Overrides all `ChangeState()` methods to call `OnEnterState()` on SaveableState components
- Manages `IsRestoring` flag to prevent OnEnterState during restoration
- Registers with SaveLoadManager via BootCompletionService
- Serializes: current state name + state data from SaveableState.SerializeState()
- Restores: changes to saved state, calls SaveableState.OnRestoreState()
**Key Features:**
```csharp
// Override ChangeState to inject OnEnterState calls
public new GameObject ChangeState(string state)
{
var result = base.ChangeState(state);
if (!IsRestoring && result != null && currentState != null)
{
var saveableState = currentState.GetComponent<SaveableState>();
if (saveableState != null)
{
saveableState.OnEnterState();
}
}
return result;
}
```
### 2. SaveableState.cs ✅
**Location:** `Assets/Scripts/Core/SaveLoad/SaveableState.cs`
**What it does:**
- Inherits from `Pixelplacement.State`
- Provides three virtual methods for save/load lifecycle:
- `OnEnterState()` - Normal gameplay initialization
- `OnRestoreState(string data)` - Silent restoration from save
- `SerializeState()` - Returns state data as JSON
**Example usage:**
```csharp
public class MyState : SaveableState
{
public override void OnEnterState()
{
// Full initialization with animations
PlayAnimation();
MovePlayer();
}
public override void OnRestoreState(string data)
{
// Silent restoration - just set positions
var saved = JsonUtility.FromJson<Data>(data);
SetPosition(saved.position);
}
public override string SerializeState()
{
return JsonUtility.ToJson(new Data { position = currentPos });
}
}
```
### 3. GardenerChaseBehavior.cs ✅
**Location:** `Assets/Scripts/Animation/GardenerChaseBehavior.cs`
**What changed:**
- Inheritance: `State``SaveableState`
- Start() logic → OnEnterState()
- Added OnRestoreState() to position without animation
- Added SerializeState() to save tween progress
### 4. StateMachineMigrationTool.cs ✅
**Location:** `Assets/Editor/StateMachineMigrationTool.cs`
**What it does:**
- Editor window: Tools → AppleHills → Migrate StateMachines to Saveable
- Scans project for all StateMachine components (prefabs + scenes)
- Shows which are already SaveableStateMachine
- One-click or batch migration
- Preserves all properties, events, and references using SerializedObject
**Fixed:** Assembly reference issues resolved by you!
---
## 🏗️ Architecture Benefits
### ✅ Zero Library Modifications
- Pixelplacement code is **100% unchanged**
- No reflection hacks in base classes
- Can update Pixelplacement library without conflicts
### ✅ Clean Separation of Concerns
- SaveableStateMachine = Save system integration (AppleHills domain)
- SaveableState = State lifecycle hooks (AppleHills domain)
- StateMachine/State = Pure state management (Pixelplacement domain)
### ✅ No Circular Dependencies
- SaveableStateMachine is in AppleHills assembly
- Can reference Core.SaveLoad freely
- No assembly boundary violations
### ✅ Opt-in Pattern
- Existing State subclasses continue working unchanged
- Only states that inherit SaveableState get save/load hooks
- States that don't need saving just inherit from State normally
### ✅ Single Component
- No wrapper confusion
- One SaveableStateMachine component per GameObject
- Clean inspector hierarchy
---
## 📖 Usage Guide
### Step 1: Migrate StateMachine Component
**Option A: Use Migration Tool**
1. Unity menu: `Tools → AppleHills → Migrate StateMachines to Saveable`
2. Click "Scan Project"
3. Click "Migrate All" or migrate individual items
4. Set "Save Id" on migrated SaveableStateMachines
**Option B: Manual Migration**
1. Remove StateMachine component
2. Add SaveableStateMachine component
3. Restore all property values
4. Set "Save Id" field
### Step 2: Update State Scripts
**For states that need save/load:**
1. Change inheritance:
```csharp
// Before:
public class MyState : State
// After:
public class MyState : SaveableState
```
2. Add using directive:
```csharp
using Core.SaveLoad;
```
3. Move Start/OnEnable logic to OnEnterState:
```csharp
// Before:
void Start()
{
InitializeAnimation();
MovePlayer();
}
// After:
public override void OnEnterState()
{
InitializeAnimation();
MovePlayer();
}
```
4. Implement OnRestoreState for silent restoration:
```csharp
public override void OnRestoreState(string data)
{
// Restore without animations/side effects
if (string.IsNullOrEmpty(data))
{
OnEnterState(); // No saved data, initialize normally
return;
}
var saved = JsonUtility.FromJson<MyData>(data);
SetPositionWithoutAnimation(saved.position);
}
```
5. Implement SerializeState if state has data:
```csharp
public override string SerializeState()
{
return JsonUtility.ToJson(new MyData
{
position = currentPosition
});
}
[System.Serializable]
private class MyData
{
public Vector3 position;
}
```
**For states that DON'T need save/load:**
- Leave them as-is (inheriting from `State`)
- They'll continue to use Start/OnEnable normally
- No changes needed!
---
## 🔄 How It Works
### Normal Gameplay Flow
```
Player interacts → SaveableStateMachine.ChangeState("Chase")
├─ base.ChangeState("Chase") (Pixelplacement logic)
│ ├─ Exit() current state
│ ├─ Enter() new state
│ │ └─ SetActive(true) on Chase GameObject
│ └─ Returns current state
├─ Check: IsRestoring? → false
└─ Call: chaseState.OnEnterState()
└─ Chase state runs full initialization
```
### Save/Load Flow
```
SaveableStateMachine.SerializeState()
├─ Get currentState.name
├─ Get saveableState.SerializeState()
└─ Return JSON: { stateName: "Chase", stateData: "..." }
SaveableStateMachine.RestoreState(data)
├─ Parse JSON
├─ Set IsRestoring = true
├─ ChangeState(stateName)
│ ├─ base.ChangeState() activates state
│ └─ Check: IsRestoring? → true → Skip OnEnterState()
├─ Call: saveableState.OnRestoreState(stateData)
│ └─ Chase state restores silently
└─ Set IsRestoring = false
```
---
## 🎓 Design Patterns Used
1. **Template Method Pattern** - SaveableState provides lifecycle hooks
2. **Strategy Pattern** - Different initialization for normal vs restore
3. **Adapter Pattern** - SaveableStateMachine adapts StateMachine to ISaveParticipant
4. **Inheritance Over Composition** - Clean, single component solution
---
## ✅ Completed Migrations
### GardenerChaseBehavior
- ✅ Inherits from SaveableState
- ✅ OnEnterState() starts tween animation
- ✅ OnRestoreState() positions without animation, resumes tween from saved progress
- ✅ SerializeState() saves tween progress and completion state
---
## 📝 Notes & Best Practices
### When to Use SaveableState
- ✅ State needs to persist data (tween progress, timers, flags)
- ✅ State has animations/effects that shouldn't replay on load
- ✅ State moves player or changes input mode
### When NOT to Use SaveableState
- ❌ Simple states with no persistent data
- ❌ States that can safely re-run Start() on load
- ❌ Decorative/visual-only states
### Common Patterns
**Pattern 1: Tween/Animation States**
```csharp
public override void OnEnterState()
{
tween = StartTween();
}
public override void OnRestoreState(string data)
{
var saved = JsonUtility.FromJson<Data>(data);
SetPosition(saved.progress);
tween = ResumeTweenFrom(saved.progress);
}
public override string SerializeState()
{
return JsonUtility.ToJson(new Data
{
progress = tween?.Percentage ?? 0
});
}
```
**Pattern 2: States with No Data**
```csharp
public override void OnEnterState()
{
PlayAnimation();
}
public override void OnRestoreState(string data)
{
// Just set final state without animation
SetAnimatorToFinalFrame();
}
// SerializeState() not overridden - returns ""
```
**Pattern 3: Conditional Restoration**
```csharp
public override void OnRestoreState(string data)
{
if (string.IsNullOrEmpty(data))
{
// No saved data - initialize normally
OnEnterState();
return;
}
// Has data - restore silently
var saved = JsonUtility.FromJson<Data>(data);
RestoreSilently(saved);
}
```
---
## 🚀 Migration Checklist
For each SaveableStateMachine:
- [ ] Replace StateMachine component with SaveableStateMachine
- [ ] Set unique Save ID in inspector
- [ ] Identify which states need save/load
- [ ] For each saveable state:
- [ ] Change inheritance to SaveableState
- [ ] Move Start/OnEnable to OnEnterState
- [ ] Implement OnRestoreState
- [ ] Implement SerializeState if has data
- [ ] Test normal gameplay flow
- [ ] Test save/load flow
---
## 🎉 Summary
**What We Built:**
- Clean inheritance pattern with zero library modifications
- Dual class hierarchy (SaveableStateMachine + SaveableState)
- Full save/load integration for state machines
- Migration tool for automatic component replacement
**Benefits:**
- ✅ No circular dependencies
- ✅ No library modifications
- ✅ Clean separation of concerns
- ✅ Opt-in pattern
- ✅ Easy to understand and maintain
**Assembly Issues:**
- ✅ Resolved by you!
**Status:**
- ✅ Zero compilation errors
- ✅ All files working correctly
- ✅ Ready for production use
---
**Documentation:** This file
**Migration Tool:** `Tools → AppleHills → Migrate StateMachines to Saveable`
**Example:** `GardenerChaseBehavior.cs`