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.

Save Slot UI

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.

Overview screenshot placeholder
  • 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.

Scalable Maintainable User-Centered Performance-Safe Subsystem-Driven
  • 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.
Save system UI demonstrating design goals

System Architecture

The save system is built using clean separation of responsibility across three layers:

Subsystem-based Design Data-Oriented UI-Isolated
Save Manager class screenshot placeholder

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.

Human-readable Slot Names UTC Timestamping Session Runtime Tracking Inventory Snapshot Per-Level Pickup State Potion Persistence
Save slots showing timestamp and progress summary

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.

UGameInstanceSubsystem Data-Oriented Event-Driven Metadata-Aware Failsafe I/O

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.

Save Subsystem C++ core logic

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.

Data-Driven Widgets Controller First Visual Hierarchy Fail-Safe UX Fast Scanning

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.

Deterministic Behavior Lower Runtime Overhead Better Error Visibility Scalable Code Architecture Engine-Level Control

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.

Systems Thinking Defensive Coding UX Awareness Data Ownership Failure Planning
Lessons learned
  • 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.

Extensible Platform-Ready Resilient User-Centric Production-Scalable
Future save system features
  • 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.