Skip to content

Recipe 2: Player Progression & Multi-Scope Systems ​

This recipe demonstrates how to organize your game's data across World, Player, and Global scopes, and how to persist plain UObject systems (like inventory managers or quest journals) that are not actors.


1. Why Scopes Matter ​

In complex RPGs and adventure games, not all data belongs in the same bucket:

text
Save Slot Container (.aegis)
├── Scope: Player  ──► Survives level transitions! (Character stats, inventory, quests)
├── Scope: World   ──► Tied to current map! (Placed doors, opened chests, defeated bosses)
└── Scope: Global  ──► Shared across saves! (Account unlocks, codex lore, achievements)
  • When the player enters a dungeon, the World scope resets or swaps to the dungeon map's state.
  • The Player scope travels with the player pawn, ensuring inventory and health are never wiped during level changes.
  • The Global scope maintains achievements and meta-currency across all save slots.

2. Persisting the Player Character (Player Scope) ​

In Blueprint ​

  1. Open your Player Character Blueprint (BP_PlayerCharacter).
  2. Add the UAegisPersistenceComponent.
  3. In the component Details panel, change Scope from World to Player.
  4. Create your player variables and tick SaveGame:
    • CurrentHealth (Float)
    • CurrentMana (Float)
    • CharacterLevel (Integer)
    • EquippedWeaponID (Name)

In C++ ​

cpp
// PlayerCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Identity/AegisPersistenceComponent.h"
#include "PlayerCharacter.generated.h"

UCLASS()
class YOURGAME_API APlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    APlayerCharacter();

protected:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Persistence")
    TObjectPtr<UAegisPersistenceComponent> Persistence;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Stats")
    float CurrentHealth = 100.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Stats")
    int32 CharacterLevel = 1;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Equipment")
    FName EquippedWeaponID = TEXT("WoodenSword");
};

// PlayerCharacter.cpp
APlayerCharacter::APlayerCharacter()
{
    Persistence = CreateDefaultSubobject<UAegisPersistenceComponent>(TEXT("Persistence"));
    
    // Set to Player Scope so this state survives level transitions!
    Persistence->SetScope(EAegisScope::Player);
    Persistence->SetShouldPersistTransform(false); // Player spawn point handles position on map load
}

3. Persisting Plain UObject Systems (Inventory & Quests) ​

Many games store inventory arrays, skill trees, or quest data in a UObject subclass rather than directly on the character actor. Because a plain UObject has no component list, we supply an identity using MakePersistentIdFromLabel.

Step 1: Create the Data Object ​

cpp
// InventoryManager.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "InventoryManager.generated.h"

USTRUCT(BlueprintType)
struct FInventorySlot
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FName ItemID = NAME_None;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    int32 Quantity = 0;
};

UCLASS(BlueprintType)
class YOURGAME_API UInventoryManager : public UObject
{
    GENERATED_BODY()

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, SaveGame, Category = "Inventory")
    TArray<FInventorySlot> Slots;

    UFUNCTION(BlueprintCallable, Category = "Inventory")
    void AddItem(FName ItemID, int32 Quantity);
};

Step 2: Register with Aegis Subsystem ​

When your game initializes the inventory (e.g., in GameInstance or PlayerState):

text
[Event: OnInitializePlayerState]
               │
               ▼
[Make Persistent Id From Label ("PlayerInventory")]
               │
               ▼
[Register Persistent Object (Aegis)]
  ├── Target Object: (MyInventoryObjectRef)
  ├── Identity: (From Label)
  └── Scope: Player
cpp
void AMyPlayerState::BeginPlay()
{
    Super::BeginPlay();

    Inventory = NewObject<UInventoryManager>(this);

    UAegisSubsystem* Aegis = UAegisSubsystem::Get(this);
    if (Aegis)
    {
        // 1. Derive deterministic GUID from stable label
        const FGuid InventoryGuid = Aegis->MakePersistentIdFromLabel(TEXT("PlayerInventory"));

        // 2. Register under Player scope
        Aegis->RegisterPersistentObject(Inventory, InventoryGuid, EAegisScope::Player);
    }
}

4. Persisting Global Meta-Progression (Global Scope) ​

To persist achievements, unlocked perks, or account-wide currency that remains unlocked regardless of which save slot or map the player loads:

cpp
// In your Achievement Subsystem or GameInstance:
UCLASS()
class UAchievementManager : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, SaveGame)
    TArray<FName> UnlockedAchievementIDs;

    virtual void Initialize(FSubsystemCollectionBase& Collection) override
    {
        Super::Initialize(Collection);

        UAegisSubsystem* Aegis = UAegisSubsystem::Get(GetWorld());
        if (Aegis)
        {
            const FGuid AchievementId = Aegis->MakePersistentIdFromLabel(TEXT("GlobalAchievements"));
            
            // Scope Global: Stored across slots!
            Aegis->RegisterPersistentObject(this, AchievementId, EAegisScope::Global);
        }
    }
};

Summary Best Practices ​

  1. Use Player Scope for Character Data: Allows seamless level transitions without data being cleared when unloading the World scope.
  2. Use Global Scope for Meta Data: Achievements, global options, and persistent spawn sequence counters belong in Global.
  3. Use Constant String Labels: Always pass static literals to MakePersistentIdFromLabel (e.g. "PlayerInventory"). Never construct dynamic labels from player display names or timestamps.

Aegis Save — Enterprise World Persistence for Unreal Engine 5.