202 lines
6.1 KiB
Markdown
202 lines
6.1 KiB
Markdown
|
|
# State Machine Save/Load Integration
|
||
|
|
|
||
|
|
**Date:** November 2, 2025
|
||
|
|
**Status:** ✅ Complete
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
Integrated the Pixelplacement StateMachine framework with the AppleHills save/load system by directly modifying the library source files and providing a clean API for state persistence.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### Two-Method Pattern
|
||
|
|
|
||
|
|
States use a clean, explicit lifecycle pattern:
|
||
|
|
|
||
|
|
1. **`OnEnterState()`** - Called when entering state during normal gameplay
|
||
|
|
2. **`OnRestoreState(string data)`** - Called when restoring state from save file
|
||
|
|
3. **`SerializeState()`** - Returns state data as JSON string for saving
|
||
|
|
|
||
|
|
### How It Works
|
||
|
|
|
||
|
|
**Normal Gameplay:**
|
||
|
|
```
|
||
|
|
Player triggers transition → ChangeState("Chase")
|
||
|
|
├─ StateMachine.Enter() activates GameObject
|
||
|
|
├─ IsRestoring = false
|
||
|
|
└─ Calls state.OnEnterState()
|
||
|
|
└─ Full initialization: animations, events, movement
|
||
|
|
```
|
||
|
|
|
||
|
|
**Save/Load:**
|
||
|
|
```
|
||
|
|
StateMachine.SerializeState()
|
||
|
|
├─ Returns current state name
|
||
|
|
└─ Calls currentState.SerializeState()
|
||
|
|
└─ State returns its internal data as JSON
|
||
|
|
|
||
|
|
StateMachine.RestoreState(data)
|
||
|
|
├─ Sets IsRestoring = true
|
||
|
|
├─ ChangeState(stateName) - activates GameObject
|
||
|
|
│ └─ Does NOT call OnEnterState() (IsRestoring=true)
|
||
|
|
├─ Calls state.OnRestoreState(stateData)
|
||
|
|
│ └─ State restores without animations/effects
|
||
|
|
└─ Sets IsRestoring = false
|
||
|
|
```
|
||
|
|
|
||
|
|
## Files Modified
|
||
|
|
|
||
|
|
### 1. State.cs
|
||
|
|
**Location:** `Assets/External/Pixelplacement/Surge/StateMachine/State.cs`
|
||
|
|
|
||
|
|
**Added:**
|
||
|
|
- `OnEnterState()` - virtual method for normal state entry
|
||
|
|
- `OnRestoreState(string data)` - virtual method for restoration
|
||
|
|
- `SerializeState()` - virtual method for serialization
|
||
|
|
|
||
|
|
### 2. StateMachine.cs
|
||
|
|
**Location:** `Assets/External/Pixelplacement/Surge/StateMachine/StateMachine.cs`
|
||
|
|
|
||
|
|
**Added:**
|
||
|
|
- Implements `ISaveParticipant` interface
|
||
|
|
- `saveId` field (serialized, set in inspector)
|
||
|
|
- `IsRestoring` property (public, readable by states)
|
||
|
|
- `HasBeenRestored` property
|
||
|
|
- Modified `Enter()` to call `OnEnterState()` when not restoring
|
||
|
|
- `SerializeState()` implementation - collects state name + state data
|
||
|
|
- `RestoreState()` implementation - restores to saved state
|
||
|
|
- Registration with SaveLoadManager via BootCompletionService
|
||
|
|
- Unregistration on destroy
|
||
|
|
|
||
|
|
### 3. GardenerChaseBehavior.cs (Example Migration)
|
||
|
|
**Location:** `Assets/Scripts/Animation/GardenerChaseBehavior.cs`
|
||
|
|
|
||
|
|
**Migrated from:**
|
||
|
|
- `Start()` method with initialization
|
||
|
|
|
||
|
|
**To:**
|
||
|
|
- `OnEnterState()` - starts chase tween
|
||
|
|
- `OnRestoreState(string)` - positions gardener without animation, resumes tween from saved progress
|
||
|
|
- `SerializeState()` - saves tween progress and completion state
|
||
|
|
|
||
|
|
## Usage Guide
|
||
|
|
|
||
|
|
### For Simple States (No Data to Save)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public class IdleState : State
|
||
|
|
{
|
||
|
|
public override void OnEnterState()
|
||
|
|
{
|
||
|
|
// Normal initialization
|
||
|
|
PlayIdleAnimation();
|
||
|
|
SubscribeToEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
public override void OnRestoreState(string data)
|
||
|
|
{
|
||
|
|
// Minimal restoration - just set visual state
|
||
|
|
SetAnimatorToIdle();
|
||
|
|
}
|
||
|
|
|
||
|
|
// SerializeState() not overridden - returns empty string by default
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### For Complex States (With Data to Save)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public class ChaseState : State
|
||
|
|
{
|
||
|
|
private float progress;
|
||
|
|
|
||
|
|
public override void OnEnterState()
|
||
|
|
{
|
||
|
|
StartChaseAnimation();
|
||
|
|
progress = 0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
public override void OnRestoreState(string data)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(data))
|
||
|
|
{
|
||
|
|
OnEnterState(); // No saved data, initialize normally
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var saved = JsonUtility.FromJson<ChaseSaveData>(data);
|
||
|
|
progress = saved.progress;
|
||
|
|
|
||
|
|
// Position objects without playing animations
|
||
|
|
SetPosition(saved.progress);
|
||
|
|
}
|
||
|
|
|
||
|
|
public override string SerializeState()
|
||
|
|
{
|
||
|
|
return JsonUtility.ToJson(new ChaseSaveData { progress = progress });
|
||
|
|
}
|
||
|
|
|
||
|
|
[System.Serializable]
|
||
|
|
private class ChaseSaveData
|
||
|
|
{
|
||
|
|
public float progress;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### For States That Don't Need Save/Load
|
||
|
|
|
||
|
|
States that don't override the new methods continue to work normally:
|
||
|
|
- Existing states using `Start()` and `OnEnable()` are unaffected
|
||
|
|
- Only states that need save/load functionality need to be migrated
|
||
|
|
|
||
|
|
## Setup in Unity
|
||
|
|
|
||
|
|
1. **Add Save ID to StateMachine:**
|
||
|
|
- Select GameObject with StateMachine component
|
||
|
|
- In inspector, set "Save Id" field to unique identifier (e.g., "GardenerStateMachine")
|
||
|
|
- Leave empty to disable saving for that state machine
|
||
|
|
|
||
|
|
2. **Migrate States:**
|
||
|
|
- For each state that needs saving:
|
||
|
|
- Move initialization logic from `Start()`/`OnEnable()` to `OnEnterState()`
|
||
|
|
- Implement `OnRestoreState()` for restoration logic
|
||
|
|
- Implement `SerializeState()` if state has data to save
|
||
|
|
|
||
|
|
## Benefits
|
||
|
|
|
||
|
|
✅ **Clean separation** - Normal vs restore logic is explicit
|
||
|
|
✅ **No timing issues** - Explicit method calls, no flag-based checks
|
||
|
|
✅ **Opt-in** - States choose to participate in save/load
|
||
|
|
✅ **Backward compatible** - Existing states work without changes
|
||
|
|
✅ **Centralized** - StateMachine manages registration automatically
|
||
|
|
✅ **State-level data** - Each state manages its own persistence
|
||
|
|
|
||
|
|
## Migration Checklist
|
||
|
|
|
||
|
|
For each state machine that needs saving:
|
||
|
|
|
||
|
|
- [ ] Set Save ID in StateMachine inspector
|
||
|
|
- [ ] Identify states that need save/load
|
||
|
|
- [ ] For each state:
|
||
|
|
- [ ] Move `Start()` logic to `OnEnterState()`
|
||
|
|
- [ ] Implement `OnRestoreState()` (handle empty data case)
|
||
|
|
- [ ] Implement `SerializeState()` if state has data
|
||
|
|
- [ ] Test normal gameplay flow
|
||
|
|
- [ ] Test save/load flow
|
||
|
|
|
||
|
|
## Completed Migrations
|
||
|
|
|
||
|
|
### ✅ GardenerChaseBehavior
|
||
|
|
- Saves tween progress and completion state
|
||
|
|
- Restores gardener position without animation
|
||
|
|
- Resumes tween from saved progress if not completed
|
||
|
|
|
||
|
|
## Notes
|
||
|
|
|
||
|
|
- All changes to Pixelplacement code are marked with `// === APPLE HILLS SAVE/LOAD INTEGRATION ===` comments
|
||
|
|
- If Pixelplacement framework is updated from GitHub, reapply these changes
|
||
|
|
- SaveLoadManager.IsRestoringState global flag is NOT used - each StateMachine has its own IsRestoring flag
|
||
|
|
- States can check `StateMachine.IsRestoring` if needed, but typically don't need to
|
||
|
|
|