Skip to content

Puzzle narrative devices

Standalone puzzle actors that hook into the rune system but carry their own state machines and scripted outcomes. The pattern: subclass ARuneableObject, expose a child ARuneSlot, watch for specific verb components, drive a beat-by-beat sequence.

Two devices ship today: the Clock and the Star Observatory. They form a paired puzzle — the clock's second hand spawning triggers the observatory's vortex.

AClockDevice (: public ARuneableObject)

Three-state machine driven by which verb component is currently attached to the device.

States (EClockState):

  • Dormant — no verb effect active.
  • SecondHandActiveURotateComponent detected.
  • FullyRunningUPassingComponent detected.

Pivots (scene components, all visible):

  • SecondHandPivot (rotates fastest)
  • MinuteHandPivot
  • HourHandPivot

Speed knobs (designer-tunable): SecondHandSpeed (default 6), MinuteHandSpeed (default 1), HourHandSpeed (default 0.2). Degrees per second around Z.

Auth:

  • CoreSlot (ARuneSlot*, EditInstanceOnly) — the slot designers point the device at. Refinement reads the rune from this slot.
  • TimeRuneData — the rune the device produces on completion.
  • RuneClassToSpawn — class for the spawned rune (typically ARune).
  • RefinementDelay (default 5) — seconds between FullyRunning entry and the Time rune appearing.

Events (BlueprintAssignable):

  • OnSecondHandStarted — fired on DormantSecondHandActive.
  • OnClockActivated — fired on SecondHandActiveFullyRunning.
  • OnTimeRuneReady — fired after RefinementDelay once FullyRunning.

Behavior summary (driven by Tick):

  • Reads the active components on itself each frame.
  • Detect Rotate → enter SecondHandActive, broadcast OnSecondHandStarted. The second hand pivot starts rotating at SecondHandSpeed. PickNextStutterTime schedules subtle stutter pauses for character; bStutterActive, StutterElapsed, StutterCooldown, StutterBaseRotation drive the hold-and-twitch beat.
  • Detect Passing → promote to FullyRunning, broadcast OnClockActivated. Minute and hour hands engage. Schedule RefinementTimerHandle for the refinement delay.
  • On timer fire: OnRefinementComplete consumes the rune in CoreSlot, spawns RuneClassToSpawn initialized with TimeRuneData, broadcasts OnTimeRuneReady.

The device is a ARuneableObject, so the rune system applies effects to it normally — Rotate and Passing get attached as component instances, the device just observes.

AStarObservatory

Coupled to a AClockDevice. Watches for OnSecondHandStarted and runs a vortex animation that spawns a Passing rune at the end.

Auth:

  • LinkedClock (AClockDevice*, EditInstanceOnly) — the clock to listen to.
  • VortexDuration (default 10) — how long the buildup takes.
  • PassingRuneData — the rune to spawn.
  • RuneClassToSpawn — class for the spawn.
  • PassingSpawnPoint (USceneComponent) — designer-placed location for the spawned rune.

State (BlueprintReadOnly):

  • VortexProgress — 0..1.
  • bVortexActive, bVortexComplete.

Events (BlueprintAssignable):

  • OnVortexStarted, OnVortexComplete, OnPassingSpawned.

Flow:

  1. BeginPlay binds OnClockSecondHandStarted to LinkedClock->OnSecondHandStarted.
  2. When the clock's second hand activates, OnClockSecondHandStarted flips bVortexActive = true.
  3. Tick advances VortexProgress toward 1 over VortexDuration seconds.
  4. At completion, CompleteVortex fires OnVortexComplete, then SpawnPassingRune instantiates RuneClassToSpawn at PassingSpawnPoint, fires OnPassingSpawned.
  5. Designer hooks the events to VFX, audio, gating logic.

The runeable-device pattern

When you want a puzzle object that also takes verb effects:

  1. Subclass ARuneableObject (not AActor).
  2. Add your visual components as scene component children.
  3. Optionally expose an EditInstanceOnly child ARuneSlot* if the device needs to consume a rune.
  4. Watch for verb components on yourself in Tick (or via the components' OnEffectApplied events if you control the components).
  5. Run your state machine and broadcast events.

The Clock is the worked example. Designers configure the slot, the speeds, and the refinement target rune; the C++ owns the state transitions.

Source files

  • Source/Rephrased_Demo/ClockDevice.h
  • Source/Rephrased_Demo/StarObservatory.h
  • Source/Rephrased_Demo/RuneableObject.h (parent class)

Known constraints

  • Clock ↔ observatory init order: The observatory binds to LinkedClock->OnSecondHandStarted in BeginPlay. Assign LinkedClock on the level instance in the Details panel — not at runtime — so the bind completes before the clock can emit OnSecondHandStarted during the same session.
  • The clock polls components in Tick rather than subscribing to attach/detach events. Cheap for one device but does not scale to many. If we add more devices, factor a "watch for component class" helper into ARuneableObject.
  • RefinementDelay is per-instance but the stutter cadence is hardcoded in PickNextStutterTime. Designers wanting to tune the pacing for new devices will need a knob.
  • Observatory→Clock coupling is one-way via delegate. The clock does not know the observatory exists. New consumers of OnSecondHandStarted plug in the same way without changes.

Change history

  • 2026-05-23 — Gabriel Li — Clock ↔ observatory BeginPlay / LinkedClock wiring note.