Save & Load System — Unreal Engine 5 (C++)
A scalable, slot-based save system built entirely in C++ for Unreal Engine 5. Designed with performance, extensibility, and UI clarity in mind—supporting metadata, world persistence, and cross-session restoration.
Project Overview
This project replaces Blueprint-based persistence with a professional-grade C++ system. The save architecture was designed like a production feature — modular systems, clean APIs, and complete UI separation from backend logic.
- Unreal Engine 5
- C++
- UI Architecture
- Serialization
- Persistence
- UX Design
Design Goals
This system was built with production constraints in mind. The primary objective was not just to "make saves work," but to design a system that remains reliable as project complexity grows — more levels, more data, and more UI states.
-
Professional UI experience
The system emulates real-world game menus with clear visual hierarchy, strong readability, and meaningful feedback for every action (save, overwrite, delete, load). -
Slot-based control
Each slot behaves like a managed resource — allowing renaming, copying, deletion, and validation while preventing accidental overwrites. -
Persistent world state
Not just player stats — doors, pickups, inventory, potion states, and per-level progression data survive across sessions. -
Metadata-driven UI
Playtime, last-played timestamp, display name, and level name are surfaced to the UI for instant slot clarity. -
Subsystem architecture
All persistence is routed through a GameInstance Subsystem to guarantee a single source of truth and deterministic data flow. -
C++ first implementation
Core systems are written in C++ for predictable performance, memory control, and debugging transparency — with Blueprint exposed only where UI needs it.
System Architecture
The save system is built using clean separation of responsibility across three layers:
This isolation ensures safe updates to UI without risking data loss or logic errors.
┌──────────────────────────────────────┐
│ UI LAYER │
│ (Save Menu Widgets) │
│ Slot Buttons • Rename • Overwrite │
└──────────────────────┬───────────────┘
│ Blueprint Calls
▼
┌──────────────────────────────────────┐
│ SAVE MANAGER LAYER │
│ (USaveSubsystem) │
│--------------------------------------│
│ SaveToSlotSync() │
│ LoadFromSlotSync() │
│ DeleteSlot() │
│ CopySlot() │
│ RenameSlot() │
│ PeekSummary() │
│ BeginSessionTimer() │
│ MarkPickupCollected() │
│--------------------------------------│
│ Maintains Session State │
│ Dispatches OnSaved / OnLoaded │
└──────────────────────┬───────────────┘
│ Unreal Engine
│ SaveGame API
▼
┌──────────────────────────────────────┐
│ DATA LAYER │
│ (UCodeSaveGame) │
│--------------------------------------│
│ PlayerScore │
│ KeysCollected │
│ LastLevel │
│ DisplayName │
│ TimePlayedSeconds │
│ SavedItems[] │
│ PotionStates (TMap) │
│ CollectedPickupsByLevel │
└──────────────────────┬───────────────┘
│ Serialization
▼
┌──────────────────────────────────────┐
│ DISK STORAGE │
│ .sav slot files on disk │
│ (PZ_Slot_0, PZ_Slot_1...) │
└──────────────────────────────────────┘
This architecture diagram illustrates the complete Save/Load pipeline. The UI layer remains fully decoupled from persistence logic, communicating only through the Save Subsystem. All data is funneled into a single SaveGame object, ensuring version safety, scalability, and predictable serialization. This design mirrors patterns used in production game pipelines.
Slot System & Runtime Metadata
Each save slot is backed by a strongly-typed UCodeSaveGame object that stores both gameplay state
and presentation-ready metadata. Save files are not treated as opaque blobs — they expose structured fields
that allow the UI to display progress, timestamps, and session history without loading full game state.
Metadata is written centrally by the USaveSubsystem to guarantee consistency across save operations
and eliminate the risk of desynchronization between runtime state and disk data.
All metadata is stamped deterministically at save-time. Runtime playtime accumulation is measured using high-resolution platform timers instead of frame-based approximation.
// Meta stamping during save
Current->LastSavedUtc = FDateTime::UtcNow();
Current->TimePlayedSeconds = GetSessionSeconds();
Slot Summaries (No Full Load Required)
Slots can be inspected using a lightweight summary query that loads only metadata, not live world state. This allows the UI menu to remain responsive and safe regardless of save size or content depth.
// Read metadata safely without restoring world state
FSaveSummary Summary = SaveSubsystem->PeekSummary(SlotIndex);
UI->SetSlotName(Summary.DisplayName);
UI->SetTimestamp(Summary.LastSavedUtc);
UI->SetProgress(Summary.PlayerScore, Summary.KeysCollected);
This summary layer prevents accidental world restoration while enabling rich UI previews of every save slot.
Persistent World Data
World persistence is handled at a per-level granularity using GUID-based tracking. Every collectible object stores a unique identifier when picked up, ensuring destroyed or collected actors do not respawn.
// GUID-based pickup tracking by level
Current->CollectedPickupsByLevel
.FindOrAdd(LevelName)
.Ids.Add(PickupId);
Potion effects use enum-based serialization rather than string parsing. This provides deterministic restore behavior and eliminates fragile runtime mapping.
Fault-Tolerant Slot Operations
Slot renaming uses an atomic copy-and-delete workflow to ensure data integrity. The original slot remains intact unless the new slot is written successfully.
// Rename = copy slot + delete original
CopySlot(FromSlot, ToSlot, OutError);
DeleteSlot(FromSlot, OutError);
This transactional model prevents corruption during write failures and mirrors production safety patterns.
C++ Highlight — Save Subsystem Implementation
Rather than embedding save logic directly inside UI or gameplay classes, this system uses a centralized Save Subsystem that acts as the single authority for persistence, slot management, and metadata handling. This prevents duplication, enforces consistency, and mirrors patterns found in production-scale projects.
Core Save / Load Flow
// ---------------- Save ----------------
FText Error;
SaveSubsystem->SaveToSlotSync(ActiveSlotIndex, Error);
// ---------------- Load ----------------
SaveSubsystem->LoadFromSlotSync(ActiveSlotIndex, Error);
The UI layer never directly touches the SaveGame API. All persistence is routed through the Save Subsystem, ensuring centralized validation, slot safety, and predictable serialization behavior.
What the Subsystem Manages
-
Slot lifecycle control
Creation, overwrite protection, copy, delete, and rename routines are encapsulated in one interface. -
Metadata stamping
Save time, playtime, level, and display name are injected automatically at write-time. -
Session timing
Wall-clock tracking ensures playtime persists across sessions and is resumed accurately after loading. -
Safe loading
Slots are validated before access. Invalid loads surface descriptive errors instead of crashing the UI. -
World-state persistence
Collected pickups, inventory, potion states, and player metrics are restored through structured data instead of raw Blueprint state reconstruction.
SaveGame Object Structure
UCLASS()
class UCodeSaveGame : public USaveGame
{
GENERATED_BODY()
public:
int32 PlayerScore;
int32 KeysCollected;
FName LastLevel;
FString DisplayName;
FDateTime LastSavedUtc;
int32 TimePlayedSeconds;
TArray<FItemStruct> SavedItems;
TMap<EPotionType, EPotionState> PotionStates;
TMap<FName, FCollectedGuidList> CollectedPickupsByLevel;
};
The SaveGame object is structured as a real data model — not a dumping ground for Blueprint variables. The layout reflects intentional design around persistent state, version safety, and extensibility.
UI / UX Design — Save Menu Experience
The Save/Load interface was designed as a functional system interface — not decoration. Every UI decision reinforces clarity, safety, and responsiveness. The menu acts as a data dashboard, allowing players to understand their progress at a glance without ever loading the world.
Visual Hierarchy & Slot Legibility
Each save slot is structured as a card. Slot content is arranged in vertical priority: identity first, progress second, and actions last.
- Slot name — Primary label
- Level/location — Context anchor
- Timestamp & playtime — Temporal reference
- Progress metrics — Score and keys collected
Safe Interaction Design
Save systems are inherently destructive — delete and overwrite operations are irreversible. The UI enforces guardrails against destructive mistakes:
- Confirmation dialogs for delete and overwrite
- Disabled actions for empty slots
- Explicit active-slot indicators
- Error feedback on failed saves
Controller-First Navigation
The entire menu is fully navigable using a controller, with spatial focus mapping and directional intent. Focus rules are deterministic — no invisible shortcuts or mouse-only logic paths.
- Directional focus movement based on slot layout
- Primary actions mapped to face buttons
- Secondary actions mapped to shoulder inputs
- Clear visual focus frames
Architecture: Data → UI (Not the Other Way Around)
Widgets do not execute save logic. They consume FSaveSummary data objects and render state only.
All commands are routed through the Save Subsystem. This enforces hard separation between UI and persistence logic.
// UI never loads the world
FSaveSummary Summary = SaveSubsystem->PeekSummary(SlotIndex);
// UI renders metadata only
UpdateSlotCard(Summary);
This architecture ensures reliability: UI changes cannot break persistence; persistence changes do not require UI rewrites.
Why C++?
While Unreal Engine enables rapid iteration with Blueprints, this project was intentionally built in C++ to demonstrate production-level engineering practices, deeper engine integration, and long-term scalability. The Save/Load system was treated as a core engine feature rather than a scripting layer convenience.
Core Reasons for a C++ Implementation
-
Performance & Memory Control
Writing the system in C++ eliminates Blueprint overhead and gives precise control over object lifetime, memory, and serialization behavior — critical for large save files and complex object graphs. -
Predictable Serialization
Data layouts, defaults, and version handling are explicitly defined in code. This prevents silent breakage from refactors and allows for clean data migrations as the project evolves. -
Debugging & Reliability
Crashes, invalid loads, and corrupted slot detection can be observed directly through call stacks and memory traces rather than opaque Blueprint failures or silent UI breakage. -
Scalable Architecture
The Save Subsystem was structured as an engine-style service rather than a monolithic script file. This allows extension (cloud saves, profiles, checkpoint systems) without rewriting foundational logic. -
Strong UI Binding
UI logic remains decoupled and data-driven: widgets query structured summaries instead of executing save logic. This keeps Blueprint usage lean and presentation-focused while C++ owns state and behavior.
Blueprint vs C++ in This Project
Blueprints remain valuable for UI layout and menu flow, but core game systems must be deterministic, testable, and extensible. In this implementation:
- Blueprints handle presentation and navigation
- C++ owns persistence, data integrity, and state restoration
- UI depends on data — never logic side-effects
This separation ensures the system remains maintainable as projects scale in complexity and content size.
What I Learned — Engineering Lessons from a Save System
Building a save system in C++ changed how I view persistence. Saving is not an isolated feature — it cuts across architecture, UI design, and runtime safety. This project reinforced that a reliable system must be designed holistically, not assembled afterward.
-
Persistence is architecture — not a feature
Saving touches input, data modeling, memory, world state, and UI. Treating it as a bolt-on system guarantees instability later. -
UX trust matters in backend systems
If players cannot trust what a save slot represents, the entire experience degrades — even if the underlying code is solid. -
Metadata is just as important as data
The save file is not only storage — it’s a communication channel between project state and the player. -
Transactional thinking prevents corruption
Designing slot operations like transactions (copy → validate → replace) prevents catastrophic loss. -
Subsystems scale better than scripts
Central authority beats scattered logic every time. This pattern applies well beyond save systems.
Future Improvements — Where This System Can Grow
The foundation is intentionally extensible. The Save Subsystem was written to grow without structural rewrite, enabling new persistence features to layer cleanly on top of existing architecture.
-
Cloud Save Support
Sync save slots across devices using platform APIs or backend services with conflict resolution rules. -
Multiple Player Profiles
Profile-scoped save directories allow multiple users on a single machine without shared slot collision. -
Visual Save Thumbnails
Capture world snapshots at save time for improved recognition inside the UI. -
Data Compression
Reduce disk footprint for large project states using binary compression before writing to disk. -
Async Save Tasks
Background save/write operations prevent frame spikes during large world serialization. -
Versioned Data Migration
Backward compatibility for legacy saves using structured versioning and safe upgrades.