Save/Load Management in Ironclad Engine
A robust Save/Load system is critical for most games, especially RPGs. The Ironclad Engine provides a SaveLoadManager.js
module designed to be flexible and extensible, allowing developers to save and restore various aspects of their game state.
Core Concepts
SaveLoadManager.js
:- This is the central module responsible for orchestrating the saving and loading processes.
- It interacts with registered "data providers" to gather data for saving and to distribute data upon loading.
- It handles the actual writing to and reading from a storage medium (currently
localStorage
). - It emits events to signal the status of save/load operations.
Data Provider Pattern:
- To make the system flexible, the
SaveLoadManager
doesn't have hardcoded knowledge of all game-specific data. Instead, different parts of the engine or game (e.g., a player manager, quest system, scene state manager) can register themselves as data providers. - Each data provider must implement two methods:
getSaveData()
: Returns a JSON-serializable object representing the current state of that provider.loadSaveData(data)
: Takes the previously saved data object and uses it to restore the provider's state.
- Providers are registered with a unique string key:
engine.saveLoadManager.registerDataProvider('mySystemKey', mySystemInstance);
- To make the system flexible, the
Storage Medium & Format:
- Format: Game state is aggregated into a single JavaScript object, which is then serialized to a JSON string.
- Storage: Currently,
localStorage
is used. This is simple for development but has size limitations (typically 5-10MB). Future enhancements might includeIndexedDB
for larger storage.
Save Slots:
- The system is designed to support multiple save slots. Each slot is identified by a unique ID (e.g., "slot1", "autosave").
- Each save file includes:
saveFileVersion
: For handling future data structure changes.timestamp
: When the save was made.metadata
: User-provided information (e.g., player name, level, current location) useful for display in a load game menu.gameData
: An object containing the data from all registered providers, keyed by their unique registration keys.
Event Emission:
- The
SaveLoadManager
(via the engine'sEventManager
) emits events like:saveGameStarted
,saveGameCompleted
,saveGameFailed
loadGameStarted
,loadGameCompleted
,loadGameFailed
saveSlotDeleted
,allSaveSlotsDeleted
- These can be used by UI scenes to provide feedback to the player.
- The
The Save Process ("Gather")
- The game (e.g., through a UI button) calls
engine.saveLoadManager.saveGame(slotId, userMetadata)
. - The
SaveLoadManager
creates a root save object, including the currentsaveFileVersion
,timestamp
, and the provideduserMetadata
. - It iterates through all registered data providers.
- For each provider, it calls
provider.getSaveData()
. - The data returned by each provider is stored in the
rootSaveObject.gameData
under the provider's unique key.javascript// Conceptual structure of saved JSON data { "saveFileVersion": "1.0.0", "timestamp": 1678886400000, "metadata": { "playerName": "Hero", "level": 5, "location": "Forest Glade" }, "gameData": { "overworldState": { /* data from OverworldScene's provider */ }, "playerCharacter": { /* data from a player character provider */ }, "questSystem": { /* data from a quest system provider */ } // ... other providers } }
- The complete
rootSaveObject
is converted to a JSON string. - The JSON string is written to
localStorage
using a key derived fromslotId
.
The Load Process ("Scatter & Restore")
Loading is generally more complex due to the need to reset and correctly re-initialize game state.
- The game calls
engine.saveLoadManager.loadGame(slotId)
. - The
SaveLoadManager
reads the JSON string fromlocalStorage
for the givenslotId
and parses it into an object (loadedRootObject
). - It checks the
saveFileVersion
(future: potential data migration). - Crucially, the
SaveLoadManager
then callsprovider.loadSaveData(dataPart)
for each registered data provider, passing the relevant chunk of data fromloadedRootObject.gameData[providerKey]
. - Game-Specific Orchestration (Very Important):
- After
loadSaveData
has been called on all providers, the game state is logically loaded into those providers. However, the visual scene and active game objects might not yet reflect this. - The
loadGame
method typically returns theloadedRootObject
or signals completion via an event (loadGameCompleted
). - The game logic (often in the scene that initiated the load, or a dedicated loading coordinator) is then responsible for:
- Transitioning to the correct scene: Based on data like
loadedRootObject.gameData.sceneState.currentSceneName
. This usually involvesengine.sceneManager.switchTo()
. - Player Placement: When the new scene initializes, it needs to place the player at the loaded coordinates. This might involve passing player position data to the scene's
initialize
method. - Entity Re-creation/State Restoration: For persistent ECS entities, they might need to be re-spawned or have their components updated based on the loaded data.
- Refreshing UI: HUDs and other UI elements need to be updated to reflect the newly loaded player stats, inventory, etc.
- Transitioning to the correct scene: Based on data like
- After
Making Your Systems Saveable
To integrate your game systems with the SaveLoadManager
:
- Identify Data: Determine what state within your system needs to persist.
- Implement
getSaveData()
:- This method in your system/class should collect all persistent state into a plain, JSON-serializable object.
- Avoid saving references to live objects directly; use IDs or other serializable representations.
- Do not save derived or easily re-calculable data.
javascript// Example in a hypothetical QuestManager class QuestManager { // ... getSaveData() { return { activeQuests: this.activeQuests.map((q) => q.serialize()), // Assuming quests have a serialize method completedQuests: [...this.completedQuestIds], questFlags: { ...this.globalQuestFlags }, } } // ... }
- Implement
loadSaveData(data)
:- This method receives the data object previously returned by
getSaveData()
. - It should clear any existing dynamic state and restore the system based on the provided
data
. - This might involve re-instantiating objects, re-populating arrays/maps, and setting flags.
javascript// Example in a hypothetical QuestManager class QuestManager { // ... loadSaveData(data) { if (!data) { this.resetToDefaultState() // Or handle as appropriate return } this.activeQuests = data.activeQuests.map((qData) => Quest.deserialize(qData, this.engine)) // Example this.completedQuestIds = new Set(data.completedQuests) this.globalQuestFlags = { ...data.questFlags } // Potentially update UI or emit events that quests have been updated } // ... }
- This method receives the data object previously returned by
- Register the Provider:
- During your system's initialization, register it with the
SaveLoadManager
:
javascript// In QuestManager.initialize(engine) // this.engine = engine; // if (this.engine.saveLoadManager) { // this.engine.saveLoadManager.registerDataProvider('questSystem', this); // }
- During your system's initialization, register it with the
Current Status & Future Work
- Framework Established: The
SaveLoadManager
with its provider pattern andlocalStorage
backend is implemented and tested for basic settings. - Pending:
- Comprehensive Data Providers: Implementing
getSaveData
/loadSaveData
for all critical game systems (player, inventory, quests, world state, ECS entities). - Robust Load Orchestration: Developing a clean process for scene transitions and full game state restoration after
loadGame()
completes. - UI for Save/Load Slots: Creating menu screens for managing saves.
- Data Versioning & Migration: For handling changes to save data structure over time.
- Alternative Storage: Exploring
IndexedDB
for larger save files.
- Comprehensive Data Providers: Implementing
The SaveLoadManager
provides a flexible foundation. The main work for a game developer using Ironclad is to ensure their game's systems correctly provide and restore their state through this API.