Save System
MCE uses a JSON-based save system with slot-based saves, versioning, and extensibility. The save system persists the player's entire game state -- roster, inventory, position, badges, quest progress, and game variables.
Architecture
The save system is composed of several components:
| Component | Purpose |
|---|---|
SavegameManager | Orchestrates save/load operations |
SavableObject | Base class for any object that needs to persist data |
MCESavesSerializer | Handles JSON serialization and deserialization |
GameVariables | Global variable store (booleans, ints, strings) |
GameVariable | Individual variable reference |
GameVariableHolder | MonoBehaviour wrapper for game variables |
SavegameInstaller | Zenject installer for the save system |
SavableGlobalGameData
The core save data container is SavableGlobalGameData (a component on the GlobalGameData GameObject). It stores:
| Data | Description |
|---|---|
| Player Position | Current scene, tile coordinates, facing direction |
| Monster Roster | All party monsters with full state (species, level, IVs, EVs, nature, moves, PP, HP, status, friendship, etc.) |
| PC Storage | Boxed monsters |
| Bag | All items and quantities |
| Money | Player's currency |
| Badges | Earned gym badges |
| MonsterDex | Seen/caught tracking per species |
| Game Variables | All GameVariable values (used by CommandGraph) |
| Quest Progress | Active quests, completed objectives, rewards claimed |
| Play Time | Total play time |
Save Slots
MCE supports multiple save slots. The default configuration provides 3 slots, but this is configurable.
Saving
// Through the SDK (all tiers)
engine.SaveSystem.Save(slotIndex); // 0, 1, or 2
// Or through the SavegameManager directly (Source tier)
savegameManager.Save(slotIndex);
Loading
// Through the SDK (all tiers)
engine.SaveSystem.Load(slotIndex);
// Check if slot has data first
if (engine.SaveSystem.HasSaveData(slotIndex))
{
engine.SaveSystem.Load(slotIndex);
}
Deleting
engine.SaveSystem.DeleteSave(slotIndex);
Save File Location
Save files are stored using Unity's Application.persistentDataPath:
| Platform | Path |
|---|---|
| Windows | %USERPROFILE%/AppData/LocalLow/<CompanyName>/<ProductName>/ |
| macOS | ~/Library/Application Support/<CompanyName>/<ProductName>/ |
| Linux | ~/.config/unity3d/<CompanyName>/<ProductName>/ |
| Android | Internal storage (app-private) |
| iOS | App sandbox |
Each slot is saved as a separate JSON file: save_0.json, save_1.json, save_2.json.
Save Versioning
MCE includes save versioning to handle upgrades between game versions:
- Each save file includes a version number.
- When loading an older save, the
MCESavesSerializerapplies migration steps to update the data structure. - This prevents save corruption when you add new fields or change data formats between updates.
Never rename serialized fields in save-related classes without providing a migration. If you rename a field, existing save files will lose that data on load. Use [FormerlySerializedAs("oldName")] for field renames.
GameVariables
The GameVariables system is MCE's global state store. It persists arbitrary key-value data that can be read and written from CommandGraph nodes, scripts, and save files.
Variable Types
| Type | Description | Example |
|---|---|---|
| Boolean | True/false flags | "defeatedBrock" = true |
| Integer | Numeric counters | "fishCaught" = 42 |
| String | Text values | "rivalName" = "Gary" |
Using in CommandGraph
- SetVariable node: Set a variable's value.
- BranchOnVariable node: Check a variable and branch.
- AddToVariable node: Increment a numeric variable.
Using in Code
// Read a variable
bool hasBadge = GameVariables.GetBool("defeatedBrock");
int fishCount = GameVariables.GetInt("fishCaught");
// Write a variable
GameVariables.SetBool("defeatedBrock", true);
GameVariables.SetInt("fishCaught", fishCount + 1);
Custom Save Data
Extending SavableObject (Source Tier)
With source code, create a new SavableObject:
public class CustomSaveData : SavableObject
{
[SerializeField] private int customCounter;
[SerializeField] private List<string> unlockedFeatures;
public override void OnSave()
{
// Gather current state before save
customCounter = MySystem.Counter;
unlockedFeatures = MySystem.GetUnlockedFeatures();
}
public override void OnLoad()
{
// Apply loaded state
MySystem.Counter = customCounter;
MySystem.SetUnlockedFeatures(unlockedFeatures);
}
}
Side-Loading Custom Data (All Tiers)
Without source access, use the save events to persist your own data alongside MCE saves:
public class MySaveExtension : MonoBehaviour
{
[Inject] private ISaveSystem saveSystem;
[Serializable]
private class MyData
{
public int highScore;
public List<string> achievements;
}
private MyData data = new MyData();
private void OnEnable()
{
saveSystem.OnSaved += OnSaved;
saveSystem.OnLoaded += OnLoaded;
}
private void OnSaved(int slot)
{
string json = JsonUtility.ToJson(data);
File.WriteAllText(GetPath(slot), json);
}
private void OnLoaded(int slot)
{
string path = GetPath(slot);
if (File.Exists(path))
{
data = JsonUtility.FromJson<MyData>(File.ReadAllText(path));
}
}
private string GetPath(int slot)
{
return Path.Combine(Application.persistentDataPath, $"custom_{slot}.json");
}
}
Auto-Save
MCE supports auto-save at configurable triggers:
- On map transition: Save when entering a new scene.
- On rest: Save when healing at a rest point.
- Timed interval: Save every N minutes.
Configure auto-save in the game configuration ScriptableObject.
- Always use a dedicated auto-save slot (e.g., slot 0) separate from manual save slots.
- Show an auto-save indicator icon when saving.
- Do not auto-save during battles or cutscenes.
Save File Format
MCE saves are plain JSON for debugging. A simplified example:
{
"version": 3,
"playerName": "Red",
"playTime": 12345.6,
"currentScene": "PalletTown",
"position": { "x": 8, "y": 12 },
"money": 5000,
"badges": ["BoulderBadge", "CascadeBadge"],
"roster": [
{
"species": 6,
"level": 36,
"nickname": "Charizard",
"currentHP": 120,
"moves": ["Flamethrower", "AirSlash", "DragonClaw", "SwordsDance"],
"ivs": [28, 31, 24, 30, 26, 31],
"evs": [0, 252, 0, 4, 0, 252]
}
],
"gameVariables": {
"defeatedBrock": true,
"defeatedMisty": true,
"fishCaught": 12
}
}
While save files are human-readable JSON, editing them manually can cause issues if you introduce invalid data (e.g., a move that does not exist in the database). Use in-game tools or debug commands instead.
Best Practices
- Test save/load roundtrips. Save, quit, reload, and verify everything is intact.
- Use GameVariables for event flags. They persist automatically and integrate with CommandGraph.
- Plan for save migration. If you add new data in an update, add a migration step.
- Provide multiple save slots. Players expect at least 3 manual save slots.
- Handle load failures gracefully. If a save file is corrupted, show a clear error rather than crashing.