410 lines
10 KiB
Markdown
410 lines
10 KiB
Markdown
|
|
# 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`
|
||
|
|
|