Adding a puzzle device¶
Designers: Configure existing devices in Blueprint before adding C++. Subclass AClockDevice or AStarObservatory in the level when possible — this recipe is for puzzle engineers extending the C++ template.
For narrative-bearing puzzle objects with their own state machine. The clock and the observatory are the templates (Puzzle narrative devices).
Pattern¶
A puzzle device:
- Is an
ARuneableObjectsubclass (so verbs can attach to it). - Owns its scene components (visual mesh, pivots, spawn points).
- Optionally references one or more
ARuneSlot*for runes the device consumes or produces. - Has a state machine driven by which verb components are currently attached.
- Broadcasts events for designers to bind VFX/audio/gating to.
Recipe¶
1. Header¶
Source/Rephrased_Demo/MyDevice.h:
#pragma once
#include "CoreMinimal.h"
#include "RuneableObject.h"
#include "MyDevice.generated.h"
class ARuneSlot;
class URotateComponent;
UENUM(BlueprintType)
enum class EMyDeviceState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Charging UMETA(DisplayName = "Charging"),
Active UMETA(DisplayName = "Active")
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMyDeviceCharged);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMyDeviceActivated);
UCLASS()
class REPHRASED_DEMO_API AMyDevice : public ARuneableObject
{
GENERATED_BODY()
public:
AMyDevice();
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
UPROPERTY(BlueprintReadOnly, Category = "My Device|State")
EMyDeviceState CurrentState = EMyDeviceState::Idle;
UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "My Device|References")
ARuneSlot* CoreSlot;
UPROPERTY(EditAnywhere, Category = "My Device|Tuning")
float ChargeDuration = 5.0f;
UPROPERTY(BlueprintAssignable, Category = "My Device|Events")
FOnMyDeviceCharged OnCharged;
UPROPERTY(BlueprintAssignable, Category = "My Device|Events")
FOnMyDeviceActivated OnActivated;
private:
float ChargeElapsed = 0.0f;
};
2. Source¶
Source/Rephrased_Demo/MyDevice.cpp:
#include "MyDevice.h"
#include "RuneSlot.h"
#include "RuneSystem/Components/RotateComponent.h"
AMyDevice::AMyDevice()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMyDevice::BeginPlay()
{
Super::BeginPlay();
CurrentState = EMyDeviceState::Idle;
}
void AMyDevice::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
switch (CurrentState)
{
case EMyDeviceState::Idle:
if (FindComponentByClass<URotateComponent>())
{
CurrentState = EMyDeviceState::Charging;
ChargeElapsed = 0.f;
}
break;
case EMyDeviceState::Charging:
ChargeElapsed += DeltaTime;
if (ChargeElapsed >= ChargeDuration)
{
CurrentState = EMyDeviceState::Active;
OnCharged.Broadcast();
OnActivated.Broadcast();
}
break;
case EMyDeviceState::Active:
// steady-state behavior, or watch for another component to advance further
break;
}
}
3. Designer setup¶
- Build, then create
BP_MyDeviceinContent/Blueprints/deriving fromAMyDevice. - Add visual components (
UStaticMeshComponentfor the body,USceneComponents for any pivots). - Configure the inherited
InitialNounData(the noun identity the device starts with — usually a custom noun likeDA_Rune_DEVICE). - Place the device in a level. Drag a slot into
CoreSlot. TuneChargeDuration. - Bind
OnCharged/OnActivatedin the level Blueprint or a coordinator BP for VFX, audio, and downstream puzzle gating.
Why subclass ARuneableObject and not AActor¶
Because the device:
- Has a noun identity (so sentences can target it).
- Wants verbs to attach as components naturally.
- Should be visible to the registry for
Find by nounqueries. - Reacts when its noun is transformed via IS (e.g. a clock that becomes "STONE" should freeze).
Plain AActor puzzle objects don't get any of that for free.
Watching for verb components¶
Two patterns:
Poll — Tick checks FindComponentByClass<UFooComponent>(). Cheap for one device, simple to read. The clock uses this.
Subscribe — Override the verb component's OnEffectApplied in a per-device subclass. Trickier because the strategy creates and destroys the component, but precise.
Use polling unless you have a measured reason not to.
Producing a rune (the clock pattern)¶
The clock spawns a Time rune after refinement. Pattern:
- Carry an
EditAnywhere URuneDataAsset* TimeRuneData. - Carry a
TSubclassOf<ARune> RuneClassToSpawn. - On state transition:
TakeRunefrom the core slot (consumes input), thenSpawnActor<ARune>(...)followed byInitializeRune(TimeRuneData). - Place the new rune at a designer-authored spawn point.
Verifying your work¶
- Place the device with the right noun identity.
- Build a calculator that hits the device with the verb your state machine expects.
- Confirm
CurrentStateadvances in the editor (useShow 3D Statsor print logs). - Confirm events fire in the bound BP graph.
- Break the sentence. The component disappears. Decide whether your state machine should also rewind or stay latched. The clock stays latched; design intent matters.
Source files¶
Source/Rephrased_Demo/RuneableObject.h— parent classSource/Rephrased_Demo/ClockDevice.h— full worked exampleSource/Rephrased_Demo/StarObservatory.h— coupled-device example using events
Change history¶
- 2026-05-23 — Gabriel Li — Designer vs engineer routing callout at top.