Skip to content

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 ​

  1. State Persistence: Remembers whether the door is open (bIsOpen) and whether it is locked (bIsLocked).
  2. Smooth Gameplay Interaction: When the player presses E, the door plays a smooth timeline, a creaking audio cue, and dust particles.
  3. Instant Silent Restoration on Load:
    • On Level Open / Stream-In: Snaps to open rotation immediately during BeginPlay before the player can see it.
    • On In-Place Checkpoint Reload: Snaps to checkpoint pose immediately via OnAegisPostLoad without reloading the level map.
  4. Zero Audio Blasts: Never plays creaking door sounds or particles when loading a save.

2. Blueprint Implementation ​

Step 1: Create the Actor Blueprint & Components ​

  1. Create an Actor Blueprint named BP_InteractiveDoor.
  2. 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]
  3. Select the AegisPersistenceComponent in the Details panel:
    • Scope: World
    • Persist Transform: False (Rotation is driven by the bIsOpen variable, not the entire actor's world transform).

Step 2: Create Variables and Tick SaveGame ​

In the My Blueprint panel, create these variables:

Variable NameTypeSaveGame Checked?Default ValueDescription
bIsOpenBooleanYesfalseTrue when the door has been swung open
bIsLockedBooleanYesfalseTrue if the door requires a key to open
OpenAngleFloatNo90.0Target Yaw angle when open (degrees)

Step 3: Implement the Aegis Persistence Events Interface ​

  1. In the toolbar, click Class Settings.
  2. Under Interfaces $\rightarrow$ Implemented Interfaces, click Add and select Aegis Persistence Events.
  3. 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 Yaw

Step 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 ​

  1. Press Play in Editor (PIE).
  2. Walk up to the door and interact to open it.
  3. In the console (~ key), type:
    text
    Aegis.Save Slot1
  4. Stop PIE (Esc).
  5. Press Play again.
  6. Open the console and type:
    text
    Aegis.Load Slot1
  7. 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 ​

Aegis Save — Enterprise World Persistence for Unreal Engine 5.