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
Worldscope resets or swaps to the dungeon map's state. - The
Playerscope travels with the player pawn, ensuring inventory and health are never wiped during level changes. - The
Globalscope maintains achievements and meta-currency across all save slots.
2. Persisting the Player Character (Player Scope)
In Blueprint
- Open your Player Character Blueprint (
BP_PlayerCharacter). - Add the
UAegisPersistenceComponent. - In the component Details panel, change Scope from
WorldtoPlayer. - 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: Playercpp
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
- Use
PlayerScope for Character Data: Allows seamless level transitions without data being cleared when unloading theWorldscope. - Use
GlobalScope for Meta Data: Achievements, global options, and persistent spawn sequence counters belong inGlobal. - Use Constant String Labels: Always pass static literals to
MakePersistentIdFromLabel(e.g."PlayerInventory"). Never construct dynamic labels from player display names or timestamps.