Game Save and Load System Implementation
PlayerPrefs.SetFloat("health", 100) works for prototypes. For production—it's a dead end. When save data volume grows to dozens of variables, PlayerPrefs becomes an unstructured dump without versioning, without multiple slot support, and without corruption protection. Migrating from PlayerPrefs to a proper system mid-development is painful.
What Save System Must Do
Minimal production-ready set:
- Multiple save slots with metadata (date, character name, level, screenshot)
- Atomic write: file is either fully written or not written—intermediate crash doesn't corrupt
- Versioning: old saves migrate on game update, don't break
- Async write: saving doesn't freeze game for 200ms
Architecture: ISaveable and SaveManager
Pattern: each component wanting to save implements ISaveable interface:
public interface ISaveable
{
string SaveId { get; }
object CaptureState();
void RestoreState(object state);
}
SaveManager on save finds all ISaveable on scene (via FindObjectsOfType or registration), calls CaptureState() on each, collects result into Dictionary<string, object>, serializes and writes to disk. On load—reverse process.
SaveId is unique string for each component. GUID generated in inspector via [SerializeField] private string _saveId. Important: don't use scene object name as ID—not unique and can change.
Serialization: JSON vs Binary
JSON (Newtonsoft.Json) is readable, easy to debug, compatible across platforms. Downsides: larger file, slightly slower, need custom converters for Unity types (Vector3, Quaternion, Color). JsonConvert.SerializeObject(data, Formatting.None) with custom UnityTypeConverter—working approach.
BinaryFormatter is built-in, fast, compact. But: deprecated in .NET 5+, has security vulnerabilities (not critical for offline games). Not recommended for new projects.
MessagePack-CSharp is binary format with JSON performance and without BinaryFormatter problems. Good choice for mobile games with large data volumes.
Save file path: Application.persistentDataPath + "/saves/slot_{index}.sav". persistentDataPath is guaranteed writable on all platforms (iOS, Android, PC, Console).
Atomic Write and Corruption Protection
Direct file overwrite via File.WriteAllText(path, json) can leave file invalid if crash during write. Atomic write:
- Write data to temp file
slot_0.sav.tmp - If successful—rename
File.Move(tmpPath, finalPath)(atomic on most OS) - Rename old file to
slot_0.sav.bakbeforehand—backup copy
On load: if main file not found or invalid—try .bak. This elementary protection saves thousands of support hours post-release.
Async Save
Serializing 5 MB JSON synchronously—50–200ms delay on mid-tier PC, worse on mobile. Solution: async/await with File.WriteAllTextAsync():
public async Task SaveAsync(int slot)
{
var data = CollectSaveData();
string json = JsonConvert.SerializeObject(data);
await File.WriteAllTextAsync(GetSavePath(slot), json);
}
In Unity async Task methods work correctly with ConfigureAwait(false) for background threads. UI save indicator shows before call, hides in finally block.
Versioning and Migration
Each save file contains "saveVersion": 3. On load, version compared with currentSaveVersion. If versions differ—run migrator chain:
ISaveMigrator[] migrators = {
new SaveMigratorV1ToV2(),
new SaveMigratorV2ToV3()
};
Each migrator knows how to update JObject from its version to next. This allows save format updates without player data loss. Without versioning, first game update changing data structure invalidates all existing saves.
Autosave and Checkpoint System
Autosave via InvokeRepeating("AutoSave", 300f, 300f)—every 5 minutes to special autosave slot. Checkpoint save: on trigger zone entry event published OnCheckpointReached, SaveManager saves to checkpoint slot without UI.
Critical: don't save during combat or loaded scene—choose save moment so background write thread doesn't compete with gameplay CPU peak. isSafeToSave flag clears during intensive scenes.
Timeline Guidelines
| Scale | Components | Timeframe |
|---|---|---|
| Simple | JSON, single slot, no versioning | 2–4 days |
| Basic | ISaveable pattern, multiple slots, atomic write | 1–2 weeks |
| Full | Async, versioning, migration, cloud sync | 3–5 weeks |
| Cloud saves | + Unity Cloud Save / Steam Cloud integration | +1–2 weeks |
Process
Start writing SaveManager with test in isolation (Unity Test Runner): save data, load, check identity. Then integrate ISaveable into existing components one by one. Simulate write crash during testing—deliberately interrupt save process and verify data didn't corrupt. Cloud sync implemented last—only after stable local saving.





