Best Practices For Saving Game Progress In Unity
Persisting Player Progression
Saving the player’s progression is an essential part of most games. To save data effectively in Unity, you first need to define what exactly needs to be saved. For an RPG, this would include things like:
- Player statistics (health, mana, strength, etc.)
- Inventory and equipment
- Quest status and progression
- Unlocked areas, levels, or modes
- Currency and purchases
Once you know what data needs to be persisted, you can choose a format to save it in. Popular options include:
- Binary: Compact and performant but not human readable. Good for saving large datasets.
- JSON: Text-based and human readable but less compact. Useful for small amounts of data.
- XML: Similar to JSON but with more verbose syntax. Adds metadata capabilities.
- Custom format: Design your own specialized format fit for your game’s specific needs.
Next, you need to serialize your gameplay data into your chosen format – converting game objects and data structures into storable text or binary data. This can be done using Unity’s built-in serialization methods including:
- JsonUtility
- BinaryFormatter
- XMLSerializer
For example, saving an inventory list in JSON could look like:
[Serializable] public class InventoryData { public string[] itemNames; public int[] itemQuantities; } InventoryData data = new InventoryData(); // Add item data.. string json = JsonUtility.ToJson(data); File.WriteAllText(GetSaveFilePath(), json);
This writes the JSON serialized data to a file, persisting it permanently so it can be reloaded later.
Strategies for Auto-Saving
Manually calling save functions requires players to consciously remember to save frequently. An auto-save system helps minimize lost progress by saving automatically at key points.
Common auto-save trigger points include:
- Scene changes: Save when switching areas or levels.
- Checkpoints: Save when player passes major landmarks or checkpoints.
- Time intervals: Save every X minutes of playtime.
- Menu interactions: Save when bringing up start menu or changing settings.
- Application focus change: Save when player minimizes or leaves the application.
Saving can be an expensive operation, so it’s best to run it in a separate thread from the main gameplay thread. This prevents hitches or stuttering during saves.
void Start() { StartCoroutine(AutoSaveCoroutine()); } IEnumerator AutoSaveCoroutine() { while (true) { SaveGame(); yield return new WaitForSeconds(60); } } async void SaveGame() { var saveTask = Task.Run(() => { // serialization code }); await saveTask; }
This schedules an auto save every 60 seconds, running serialization off the main thread.
Optimization for Large Save Files
Certain genres like open world RPGs can accumulate large amounts of save data. Serializing all that data to storage can lead to long save times. Some optimization strategies include:
- Compression: Use an algorithm like GZip or LZ4 to compress save data.
- Incremental saving: Only serialize data that has changed instead of rewriting entire files.
- Partitioning: Split data across multiple smaller files instead of one large one.
For example, you could periodically checkpoint only changed data:
[Serializable] class PlayerData { public int health; public Vector3 position; // Other fields.. } PlayerData baseData; PlayerData incrementalData; // Initially.. baseData = new PlayerData(); Save(baseData); // At checkpoint.. incrementalData = new PlayerData(); incrementalData.health = player.health; incrementalData.position = player.position; Save(incrementalData);
This saves only the changing health and position values at each checkpoint while retaining all other static data.
Handling Save Errors Gracefully
Save operations are vulnerable to errors and exceptions. The game should handle these scenarios gracefully to avoid losing player progress or getting into broken states. Some best practices include:
- Use try/catch blocks around serialization code to catch errors.
- Inform players if a save fails via on-screen messages.
- Implement backup/retry logic to attempt saving again before giving up.
- Support multiple save slots so failures don’t result in completely lost data.
For example:
void SavePlayer() { try { Serialize(playerData); ShowMessage("Game Saved"); } catch { ShowMessage("Failed to save.. Retrying"); Serialize(backupSlot); if (success) { ShowMessage("Retry successful!"); } else { ShowMessage("Unable to save game.."); } } }
This gives the player feedback on save issues and tries fallback solutions before admitting failure.
Restoring Saved Games
Once data is saved, you need to be able to reload it seamlessly as well. This involves:
- Providing intuitive menus/UIs for players to locate and load saves.
- Reading save data back from storage and deserializing it.
- Error checking loaded data – catch corrupted or missing files.
- Reconstructing game state from loaded data – respawn entities, rebuild scenes, etc.
For example:
PlayerData loadedData; void LoadPlayer() { if (File.Exists(saveFilePath)) { try { string json = File.ReadAllText(saveFilePath); loadedData = JsonUtility.FromJson(json); } catch { Debug.LogError("Failed to load save file!"); return; } ValidateLoadedData(loadedData); RestorePlayerState(loadedData); } else { Debug.Log("No save file found.."); } }
This handles cases where save data is invalid or missing entirely, failing gracefully to avoid crashing the game.