Recipe: Interactive Door & Moving Props
This recipe provides a complete, production-ready implementation of an interactive hinged or sliding door (BP_InteractiveDoor).
It demonstrates how to persist open/closed states, play smooth timeline animations on interaction, and instantly restore 3D mesh poses without audio blasts or visual pop-in when loading a save.
1. Feature Specifications
- State Persistence: Remembers whether the door is open (
bIsOpen) and whether it is locked (bIsLocked). - Smooth Gameplay Interaction: When the player presses
E, the door plays a smooth timeline, a creaking audio cue, and dust particles. - Instant Silent Restoration on Load:
- On Level Open / Stream-In: Snaps to open rotation immediately during
BeginPlaybefore the player can see it. - On In-Place Checkpoint Reload: Snaps to checkpoint pose immediately via
OnAegisPostLoadwithout reloading the level map.
- On Level Open / Stream-In: Snaps to open rotation immediately during
- Zero Audio Blasts: Never plays creaking door sounds or particles when loading a save.
2. Blueprint Implementation
Step 1: Create the Actor Blueprint & Components
- Create an Actor Blueprint named
BP_InteractiveDoor. - Configure the component hierarchy:text
[RootComponent: Scene or StaticMesh: DoorFrame] (Mobility = Static or Movable) ├── [StaticMesh: DoorSlab] (Mobility = Movable! This is the rotating door panel) ├── [BoxCollision: InteractionTrigger] (For player overlap / interaction prompt) └── [UAegisPersistenceComponent: Persistence] - Select the
AegisPersistenceComponentin the Details panel:- Scope:
World - Persist Transform:
False(Rotation is driven by thebIsOpenvariable, not the entire actor's world transform).
- Scope:
Step 2: Create Variables and Tick SaveGame
In the My Blueprint panel, create these variables:
| Variable Name | Type | SaveGame Checked? | Default Value | Description |
|---|---|---|---|---|
bIsOpen | Boolean | Yes | false | True when the door has been swung open |
bIsLocked | Boolean | Yes | false | True if the door requires a key to open |
OpenAngle | Float | No | 90.0 | Target Yaw angle when open (degrees) |
Step 3: Implement the Aegis Persistence Events Interface
- In the toolbar, click Class Settings.
- Under Interfaces $\rightarrow$ Implemented Interfaces, click Add and select
Aegis Persistence Events. - Compile the Blueprint.
Step 4: Create the UpdateDoorVisuals Function
Create a new function named UpdateDoorVisuals with one input parameter:
- Input:
bInstant(Boolean)
text
Function: UpdateDoorVisuals(bInstant: Boolean)
│
▼
[Branch: bInstant == true?]
│
├── TRUE (Snap Pose Instantly — Used for Load & BeginPlay):
│ │
│ ▼
│ [Branch: bIsOpen == true?]
│ ├── True ──► [Set Relative Rotation on DoorSlab: Yaw = OpenAngle (90°)]
│ └── False ──► [Set Relative Rotation on DoorSlab: Yaw = 0.0°]
│
└── FALSE (Play Smooth Animation — Used for Player Interaction):
│
▼
[Branch: bIsOpen == true?]
├── True ──► [Play Sound at Location: "DoorCreak_Open"]
│ [Timeline: Play From Start (0° to 90°)] ──► Update DoorSlab Yaw
│
└── False ──► [Play Sound at Location: "DoorCreak_Close"]
[Timeline: Reverse From End (90° to 0°)] ──► Update DoorSlab YawStep 5: Connect the 3 Lifecycle Events
In your Event Graph, connect UpdateDoorVisuals to the three critical lifecycle entry points:
1. Player Interaction (Smooth Timeline + Audio)
text
[Custom Event: OnPlayerInteract]
│
▼
[Branch: bIsLocked == true?]
├── True ──► [Play Audio: "Door_LockedRattle"] ──► [Show UI: "Door is Locked!"]
└── False ──► [Set bIsOpen = NOT bIsOpen]
│
▼
[UpdateDoorVisuals (bInstant = false)]
│
▼
[Mark Changed (Aegis)] (Flags this door dirty for next incremental save)2. Level Open / Map Load (BeginPlay)
text
[Event BeginPlay]
│
▼
[UpdateDoorVisuals (bInstant = true)] (Snaps to loaded pose before first tick!)3. In-Place Checkpoint Reload (On Aegis Post Load)
text
[Event On Aegis Post Load] (Interface Event)
│
▼
[UpdateDoorVisuals (bInstant = true)] (Snaps to checkpoint pose without reloading map!)3. C++ Implementation
Here is the complete, self-contained C++ implementation:
cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Identity/AegisPersistenceComponent.h"
#include "Identity/AegisPersistenceEvents.h"
#include "Components/TimelineComponent.h"
#include "InteractiveDoor.generated.h"
class UStaticMeshComponent;
class UBoxComponent;
class USoundBase;
UCLASS()
class YOURGAME_API AInteractiveDoor : public AActor, public IAegisPersistenceEvents
{
GENERATED_BODY()
public:
AInteractiveDoor();
/** Triggers player interaction (e.g. pressing 'E') */
UFUNCTION(BlueprintCallable, Category = "Door")
void Interact(APawn* InstigatorPawn);
protected:
virtual void BeginPlay() override;
// --- IAegisPersistenceEvents Interface ---
virtual void OnAegisPostLoad_Implementation() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> DoorFrameMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> DoorSlabMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UBoxComponent> InteractionTrigger;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Persistence")
TObjectPtr<UAegisPersistenceComponent> Persistence;
// --- Saved Properties ---
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Door|State")
bool bIsOpen = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Door|State")
bool bIsLocked = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door|Config")
float OpenAngle = 90.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door|Audio")
TObjectPtr<USoundBase> OpenSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door|Audio")
TObjectPtr<USoundBase> CloseSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door|Audio")
TObjectPtr<USoundBase> LockedSound;
void UpdateVisuals(bool bInstant);
};cpp
#include "InteractiveDoor.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Kismet/GameplayStatics.h"
AInteractiveDoor::AInteractiveDoor()
{
PrimaryActorTick.bCanEverTick = false;
DoorFrameMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DoorFrame"));
RootComponent = DoorFrameMesh;
DoorSlabMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DoorSlab"));
DoorSlabMesh->SetupAttachment(RootComponent);
DoorSlabMesh->SetMobility(EComponentMobility::Movable);
InteractionTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("InteractionTrigger"));
InteractionTrigger->SetupAttachment(RootComponent);
Persistence = CreateDefaultSubobject<UAegisPersistenceComponent>(TEXT("Persistence"));
Persistence->SetScope(EAegisScope::World);
Persistence->SetShouldPersistTransform(false); // Driven by bIsOpen
}
void AInteractiveDoor::BeginPlay()
{
Super::BeginPlay();
// 1. On Level Open / Stream-In:
// Aegis restored bIsOpen before BeginPlay (AC-303).
// Apply the visual pose instantly with zero sound!
UpdateVisuals(/*bInstant=*/ true);
}
void AInteractiveDoor::OnAegisPostLoad_Implementation()
{
// 2. On In-Place Checkpoint Reload:
// When reloading mid-game without reloading the level map,
// apply the updated state instantly!
UpdateVisuals(/*bInstant=*/ true);
}
void AInteractiveDoor::Interact(APawn* InstigatorPawn)
{
if (bIsLocked)
{
if (LockedSound)
{
UGameplayStatics::PlaySoundAtLocation(this, LockedSound, GetActorLocation());
}
return;
}
// Toggle open state
bIsOpen = !bIsOpen;
// Play smooth animation + audio
UpdateVisuals(/*bInstant=*/ false);
// Notify Aegis that this actor is dirty for the next autosave
if (Persistence)
{
Persistence->MarkChanged();
}
}
void AInteractiveDoor::UpdateVisuals(bool bInstant)
{
const FRotator TargetRot(0.0f, bIsOpen ? OpenAngle : 0.0f, 0.0f);
if (bInstant)
{
// Snap immediately (no audio, no particles)
DoorSlabMesh->SetRelativeRotation(TargetRot);
}
else
{
// Smooth player interaction
DoorSlabMesh->SetRelativeRotation(TargetRot);
USoundBase* const SoundToPlay = bIsOpen ? OpenSound : CloseSound;
if (SoundToPlay)
{
UGameplayStatics::PlaySoundAtLocation(this, SoundToPlay, GetActorLocation());
}
}
}4. Testing Your Door in 30 Seconds
- Press Play in Editor (PIE).
- Walk up to the door and interact to open it.
- In the console (
~key), type:textAegis.Save Slot1 - Stop PIE (
Esc). - Press Play again.
- Open the console and type:text
Aegis.Load Slot1 - Observe the Door: The door slab will instantly snap to the opened angle with zero sound effects, perfectly matching the state when you saved!
Next Steps
- Learn how state and visuals separate in Using Loaded Data (State vs. Visuals).
- See how container inventories and loot dropping work in Recipe 1: Interactive Loot Chest.
- Check out the Level Designer & Artist Guide for more prop setups.