Recipe 1: Interactive Loot Chest & Container
This recipe demonstrates how to build a fully persistent interactive treasure chest. It persists its open/closed state, inventory contents, and handles destruction safely without duplicating loot drops on load.
1. Feature Requirements
- Persistent State: The chest remembers if it has been opened (
bIsOpen), its remaining items (StoredItems), and its gold count. - Animation / Mesh State: On load, if
bIsOpen == true, the chest immediately displays its open lid pose without replaying the opening sound. - Loot Dropping: Interacting opens the chest and spawns loot items in front of it.
- Destruction & Load Safety: If smashed by a player, it drops any remaining loot and is destroyed. When a save is loaded,
WasDestroyedByLoadensures that loot and death particles are not re-spawned.
2. Blueprint Implementation
Step 1: Create the Actor Blueprint
- Create a new Actor Blueprint named
BP_LootChest. - Add a
StaticMeshComponentfor the chest base and a childStaticMeshComponentfor the chest lid. - Add the
UAegisPersistenceComponent. - In the Persistence Component details, leave
Scope = WorldandbPersistTransform = True.
Step 2: Define Variables and Tag Them
Create the following variables in the My Blueprint panel, and tick SaveGame in their Advanced details:
| Variable Name | Type | SaveGame Ticked? | Default Value | Purpose |
|---|---|---|---|---|
bIsOpen | Boolean | Yes | false | Tracks open/closed state |
GoldAmount | Integer | Yes | 150 | Gold stored inside |
LootItemIDs | Array of Names | Yes | ["Sword_01", "Potion_Health"] | Items stored inside |
Step 3: Interaction Logic (Open & Drop Loot)
text
[Event: OnPlayerInteract]
│
▼
[Branch: bIsOpen == false?]
├── False ──► (Already open; do nothing)
└── True ──► [Set bIsOpen = true]
│
▼
[Spawn Loot Actors in World]
[Play Sound: "ChestOpen"]
[Play Open Timeline (Rotate Lid 90°)]
│
▼
[Mark Changed (Aegis)] (Notifies Aegis this chest is dirty!)Step 4: BeginPlay (Restoring Visual State)
When the level loads or streams in, restore the lid angle without playing the opening chime:
text
[Event BeginPlay]
│
▼
[Branch: bIsOpen == true?]
├── True ──► [Set Lid Relative Rotation: Pitch=90°] (Already opened; snap open!)
└── False ──► [Set Lid Relative Rotation: Pitch=0°] (Still closed)Step 5: EndPlay (Guarding Against Duplicate Loot on Load)
text
[Event End Play]
│
▼
[Was Destroyed By Load (Aegis)]
│
▼
[Branch]
├── True ──► (Actor cleaned up by save load; DO NOTHING!)
└── False ──► [Spawn Remaining Loot] ──► [Spawn Splinter Debris FX]3. C++ Implementation
Here is the complete, production-ready C++ implementation:
cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Identity/AegisPersistenceComponent.h"
#include "Identity/AegisPersistenceEvents.h"
#include "InteractiveChest.generated.h"
UCLASS()
class YOURGAME_API AInteractiveChest : public AActor, public IAegisPersistenceEvents
{
GENERATED_BODY()
public:
AInteractiveChest();
/** Triggers player interaction */
UFUNCTION(BlueprintCallable, Category = "Chest")
void Interact(APawn* InstigatorPawn);
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// --- IAegisPersistenceEvents Interface ---
virtual int32 GetAegisStateRevision_Implementation() const override;
// --- Components ---
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> ChestBaseMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> ChestLidMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Persistence")
TObjectPtr<UAegisPersistenceComponent> PersistenceComponent;
// --- Persistent Gameplay Properties (Marked SaveGame) ---
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Chest|Data")
bool bIsOpen = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Chest|Data")
int32 GoldAmount = 150;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Chest|Data")
TArray<FName> StoredItemIDs;
private:
/** Internal state revision counter for sub-millisecond change detection */
UPROPERTY(SaveGame)
int32 StateRevision = 0;
void ApplyVisualOpenState(bool bAnimate);
void DropLootItems();
};cpp
#include "InteractiveChest.h"
#include "Core/AegisSubsystem.h"
#include "Kismet/GameplayStatics.h"
AInteractiveChest::AInteractiveChest()
{
PrimaryActorTick.bCanEverTick = false;
ChestBaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ChestBase"));
RootComponent = ChestBaseMesh;
ChestLidMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ChestLid"));
ChestLidMesh->SetupAttachment(RootComponent);
// Attach Aegis Persistence
PersistenceComponent = CreateDefaultSubobject<UAegisPersistenceComponent>(TEXT("PersistenceComponent"));
PersistenceComponent->SetScope(EAegisScope::World);
PersistenceComponent->SetShouldPersistTransform(true);
// Populate initial default loot
StoredItemIDs.Add(TEXT("IronSword"));
StoredItemIDs.Add(TEXT("HealthPotion"));
}
void AInteractiveChest::BeginPlay()
{
Super::BeginPlay();
// If loaded as already opened, snap lid open immediately without re-animating
if (bIsOpen)
{
ApplyVisualOpenState(/*bAnimate=*/ false);
}
}
void AInteractiveChest::Interact(APawn* InstigatorPawn)
{
if (bIsOpen)
{
return; // Already looted
}
bIsOpen = true;
StateRevision++; // Bump revision counter for change detection
ApplyVisualOpenState(/*bAnimate=*/ true);
DropLootItems();
// Inform Aegis that this actor is dirty
if (UAegisSubsystem* Aegis = UAegisSubsystem::Get(this))
{
Aegis->MarkChanged(this);
}
}
int32 AInteractiveChest::GetAegisStateRevision_Implementation() const
{
// Return revision counter so Aegis skips serializing this chest when unchanged!
return StateRevision;
}
void AInteractiveChest::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// CRITICAL: Check if destroyed because a loaded save marked it dead
UAegisSubsystem* Aegis = UAegisSubsystem::Get(this);
if (Aegis && Aegis->WasDestroyedByLoad(this))
{
// Cleaned up by save load; skip loot drops and particle effects!
return;
}
// Normal destruction during active gameplay:
if (!bIsOpen)
{
DropLootItems();
}
}
void AInteractiveChest::ApplyVisualOpenState(bool bAnimate)
{
const FRotator OpenRotation(0.0f, 0.0f, -90.0f);
ChestLidMesh->SetRelativeRotation(OpenRotation);
}
void AInteractiveChest::DropLootItems()
{
// Gameplay logic to spawn pickup actors in front of the chest
StoredItemIDs.Empty();
GoldAmount = 0;
}4. Key Takeaways from this Recipe
StateRevisionOptimization: By implementingIAegisPersistenceEvents::GetAegisStateRevision, Aegis only serializes this chest whenInteractincrements the revision. In an open world with 5,000 chests, 4,999 of them cost 0 microseconds during autosaves!WasDestroyedByLoad: Always check this method inEndPlayto ensure that loaded games don't duplicate loot drops.- Restoring State in
BeginPlay: Check yourSaveGamebooleans inBeginPlayto set visual state (such as opening meshes, lighting torches, or playing particle loops).