A save system is one of those features that seems simple until you build one. "Just save the game state and load it back" sounds straightforward. Then you discover you need to handle hundreds of actors, serialization edge cases, async file I/O, save file corruption, versioning, and platform-specific storage requirements.
This guide covers the full picture — from basic concepts to production-ready patterns.
What to Save (and What Not To)
The first decision is what actually goes into a save file. The instinct is to save everything. Resist it.
Always Save
- Player position and rotation — where they are in the world
- Player stats — health, mana, experience, level, attribute values
- Inventory contents — items, quantities, equipment slots
- Quest state — active quests, completed objectives, quest flags
- Dialogue state — NPC memory flags, knowledge flags
- World state changes — doors opened, items picked up, enemies killed, switches toggled
- Game settings — difficulty, accessibility options, control bindings
Reconstruct Instead of Saving
- Static level geometry — the level itself doesn't change, so don't save it. Just save which level to load.
- Default actor properties — only save properties that differ from the actor's default state. If a door's default state is closed, only save doors that have been opened.
- Derived values — if max health is calculated from level and constitution, save level and constitution, not max health. Derived values should be recalculated on load.
- Visual state — animations, particle effects, and UI state are transient. Reconstruct them from game state after loading.
The Delta Pattern
Save only the difference from the default state. For a level with 500 actors, you might only save data for 30 that have changed. This keeps save files small and load times fast.
The pattern:
- Level loads with all actors in their default state
- Save system applies saved deltas to actors that have changed
- Actors without saved data remain in their default state
This also handles new content gracefully — if you add new actors in an update, they appear in their default state without breaking existing saves.
Serialization Strategies
UE5's Built-in Serialization
Unreal provides FArchive for binary serialization and FJsonObject for JSON. Each has tradeoffs:
Binary (FArchive):
- Smaller file sizes
- Faster read/write
- Not human-readable (harder to debug)
- Version-sensitive — field order and types must match exactly
JSON:
- Human-readable (easy to debug and test)
- More tolerant of version changes
- Larger file sizes
- Slower to parse
Our recommendation: JSON for development, with the option to switch to binary for shipping. The debugging convenience of readable save files during development is worth the performance cost. Most indie games don't have save files large enough for the difference to matter.
The SaveGame UObject
UE5 provides USaveGame as a base class for save data. Create a subclass, add UPROPERTY fields for everything you need to save, and use UGameplayStatics::SaveGameToSlot / LoadGameFromSlot.
This is the simplest approach and works well for small games. Limitations appear when:
- You need async saving (the built-in functions are synchronous)
- Save files grow large (serialization time blocks the game thread)
- You need fine-grained control over what gets serialized
Custom Serialization
For production systems, you'll often want custom serialization that:
- Serializes only changed actors (delta pattern)
- Handles async file I/O
- Supports versioning
- Validates data integrity
This is where the complexity lives. Each actor type needs a serialization interface that knows how to write its changed state and restore it.
Async Saving
Synchronous saving freezes the game while the file writes. For small save files, the freeze is imperceptible. For large open-world games with hundreds of changed actors, it can cause visible hitches.
The Async Pattern
- Gather data on the game thread — iterate through actors, collect changed state
- Serialize on a worker thread — convert to binary or JSON off the main thread
- Write to disk on a worker thread — file I/O off the main thread
- Notify the game thread on completion — update UI, clear saving indicator
The critical constraint: gathering actor data must happen on the game thread (actors aren't thread-safe). Serialization and file I/O can happen off-thread.
Autosave Considerations
If your game autosaves, the async pattern is essential. An autosave that freezes the game every 5 minutes is unacceptable. The player should never notice an autosave happening.
Trigger autosaves at low-activity moments — entering a new zone, after a dialogue ends, at checkpoints. Avoid autosaving during combat or platforming.
Save File Management
Save Slots
Most games need multiple save slots. The save slot system should handle:
- Slot metadata — player name, playtime, level, timestamp, screenshot thumbnail
- Slot enumeration — list available saves for the load screen
- Slot deletion — with confirmation dialog
- Quick save / quick load — dedicated slot that overwrites on each quick save
Store metadata separately from save data so the load screen can display slot information without deserializing full save files.
File Corruption
Save files can become corrupted from crashes during write, disk errors, or bugs in serialization code. Protect against this:
Write-rename pattern: Write the save to a temporary file, then rename it to the target filename. If the write crashes, the original save file is untouched. If the rename crashes, the temporary file is still valid.
Backup saves: Maintain one backup of the previous save. Before overwriting slot 1, copy the current slot 1 to slot 1 backup. If the new save is corrupted, the player loses progress but not their entire save.
Checksum validation: Write a checksum (CRC32 or SHA-256) at the end of the save file. On load, validate the checksum before deserializing. If validation fails, try the backup.
Save File Location
Use platform-appropriate save directories:
- Windows:
FPaths::ProjectSavedDir()or the user's AppData folder - Steam: Steam Cloud handles sync if you configure it
- Console: Each platform has mandatory save data requirements — follow platform TRCs/XRs
Version Compatibility
Your save format will change during development. New features add fields. Refactoring renames them. Old saves need to either load correctly or fail gracefully.
Version Numbers
Include a version number in every save file. On load:
- Read the version number first
- If version matches current, deserialize normally
- If version is older, run a migration function
- If version is newer (somehow), reject the save with a clear error
Migration Functions
For each version increment, write a function that converts the old format to the new one:
v1 → v2: add "player_level" field with default value 1
v2 → v3: rename "hp" to "current_health"
v3 → v4: split "inventory" into "equipment" and "consumables"
Chain migrations: a v1 save loads by running v1→v2, then v2→v3, then v3→v4. This means you only maintain one migration per version change, not one per version pair.
Breaking Changes
Some changes can't be migrated. If you fundamentally restructure your game systems, old saves may be incompatible. This is acceptable during early access with clear communication. It's unacceptable post-release.
Design your save format with future changes in mind. Use named fields (JSON keys, not positional binary), include a version number from day one, and avoid saving derived data.
The Pre-Built Approach
Building all of this from scratch takes weeks. The Blueprint Template Library's Save/Load system handles:
- Full game state serialization — player data, inventory, quests, dialogue state, world changes
- Async save/load — no frame hitches
- Multiple save slots with metadata
- Version handling — graceful migration support
- Integration with other systems — the save system knows about the inventory, quest, and dialogue systems automatically
For solo developers and small teams, using a pre-built save system lets you focus on your game's unique content rather than infrastructure.
Common Pitfalls
Saving too much. Large save files mean slow saves and slow loads. Use the delta pattern. Save changes, not world state.
Not testing save/load early. The longer you wait to test saves, the more systems you'll need to retrofit serialization into. Test save/load as soon as you have game state worth saving.
Ignoring async. Synchronous saves work until they don't. By the time you notice the hitch, refactoring to async is painful. Start async if your game will have significant state.
No corruption handling. Players lose their save files and blame your game, not their hard drive. The write-rename pattern and backup saves are simple insurance against the worst-case scenario.
Platform-specific issues. Steam Cloud, console save requirements, and mobile storage all have specific constraints. Research your target platforms early.
Getting Started
If you're early in development, implement a basic USaveGame system first. It takes an hour and handles simple games well. As your needs grow, evaluate whether to build a custom system or adopt a pre-built one like the Blueprint Template Library's save system.
Check out the save system documentation for implementation details and integration guides.
A good save system is invisible. Players should never think about it — they just trust that their progress is safe.