
State Machine with NPC Behavior: A Pirate Lookout on the Clifftop
Overview
A state machine is the backbone of believable NPC behavior. Instead of one monolithic script, you divide the NPC's life into discrete states (Idle, Alert, Reset) and define the transitions between them. In UEFN, you wire this up by:
- Subclassing
npc_behavior— Epic's abstract base class for custom NPC logic. Attach it to an NPC Spawner device via a Character Definition asset. - Subscribing to
trigger_device.TriggeredEvent— so the NPC reacts when a player steps on a pressure plate near the clifftop. - Driving a
timer_device— to count down the alert window and fireSuccessEventwhen time is up, transitioning the NPC back to idle.
When to reach for this pattern:
- You want an NPC that reacts to player proximity or actions.
- You need timed state windows (alert lasts 15 seconds, then resets).
- You want to chain multiple device events without Blueprint spaghetti.
API Reference
npc_behavior
Inherit from this to create a custom NPC behavior. The npc_behavior can be defined for a character in a CharacterDefinition asset, or in a npc_spawner_device.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
npc_behavior<native><public> := class<abstract>:
trigger_device
Used to relay events to other linked devices.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse). Inherited members are merged from trigger_base_device.
trigger_device<public> := class<concrete><final>(trigger_base_device):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
TriggeredEvent |
TriggeredEvent<public>:listenable(?agent) |
Signaled when an agent triggers this device. Sends the agent that used this device. Returns false if no agent triggered the action (ex: it was triggered through code). |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Trigger |
Trigger<public>(Agent:agent):void |
Triggers this device with Agent being passed as the agent that triggered the action. Use an agent reference when this device is setup to require one (for instance, you want to trigger the device only with a particular agent. |
Trigger |
Trigger<public>():void |
Triggers this device, causing it to activate its TriggeredEvent event. |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>():void |
Disables this device. |
SetMaxTriggerCount |
SetMaxTriggerCount<public>(MaxCount:int):void |
Sets the maximum amount of times this device can trigger. * 0 can be used to indicate no limit on trigger count. * MaxCount is clamped between [0,20]. |
GetMaxTriggerCount |
GetMaxTriggerCount<public>()<transacts>:int |
Gets the maximum amount of times this device can trigger. * 0 indicates no limit on trigger count. |
GetTriggerCountRemaining |
GetTriggerCountRemaining<public>()<transacts>:int |
Returns the number of times that this device can still be triggered before hitting GetMaxTriggerCount. Returns 0 if GetMaxTriggerCount is unlimited. |
SetResetDelay |
SetResetDelay<public>(Time:float):void |
Sets the time (in seconds) after triggering, before the device can be triggered again (if MaxTrigger count allows). |
GetResetDelay |
GetResetDelay<public>()<transacts>:float |
Gets the time (in seconds) before the device can be triggered again (if MaxTrigger count allows). |
SetTransmitDelay |
SetTransmitDelay<public>(Time:float):void |
Sets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
GetTransmitDelay |
GetTransmitDelay<public>()<transacts>:float |
Gets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
timer_device
Provides a way to keep track of the time something has taken, either for scoreboard purposes, or to trigger actions. It can be configured in several ways, either acting as a countdown to an event that is triggered at the end, or as a stopwatch for an action that needs to be completed before a set time runs out.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
timer_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
SuccessEvent |
SuccessEvent<public>:listenable(?agent) |
Signaled when the timer completes or ends with success. Sends the agent that activated the timer, if any. |
FailureEvent |
FailureEvent<public>:listenable(?agent) |
Signaled when the timer completes or ends with failure. Sends the agent that activated the timer, if any. |
StartUrgencyModeEvent |
StartUrgencyModeEvent<public>:listenable(?agent) |
Signaled when the timer enters Urgency Mode. Sends the agent that activated the timer, if any. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Enable |
Enable<public>(Agent:agent):void |
Enables this device for Agent. |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>(Agent:agent):void |
Disables this device for Agent. While disabled this device will not receive signals. |
Disable |
Disable<public>():void |
Disables this device. While disabled this device will not receive signals. |
ResetForAll |
ResetForAll<public>(Agent:agent):void |
Resets the timer back to its base time and stops it for all agents. |
ResetForAll |
ResetForAll<public>():void |
Resets the timer back to its base time and stops it for all agents. |
Start |
Start<public>(Agent:agent):void |
Starts the timer for Agent. |
Start |
Start<public>():void |
Starts the timer. |
Pause |
Pause<public>(Agent:agent):void |
Pauses the timer for Agent. |
Pause |
Pause<public>():void |
Pauses the timer. |
Resume |
Resume<public>(Agent:agent):void |
Resumes the timer for Agent. |
Resume |
Resume<public>():void |
Resumes the timer. |
Complete |
Complete<public>(Agent:agent):void |
Completes the timer for Agent. |
Complete |
Complete<public>():void |
Completes the timer. |
StartForAll |
StartForAll<public>(Agent:agent):void |
Starts the timer for all agents. |
StartForAll |
StartForAll<public>():void |
Starts the timer for all agents. |
PauseForAll |
PauseForAll<public>(Agent:agent):void |
Pauses the timer for all agents. |
PauseForAll |
PauseForAll<public>():void |
Pauses the timer for all agents. |
ResumeForAll |
ResumeForAll<public>(Agent:agent):void |
Resumes the timer for all agents. |
ResumeForAll |
ResumeForAll<public>():void |
Resumes the timer for all agents. |
CompleteForAll |
CompleteForAll<public>(Agent:agent):void |
Completes the timer for all agents. |
CompleteForAll |
CompleteForAll<public>():void |
Completes the timer for all agents. |
Save |
Save<public>(Agent:agent):void |
Saves this device's data for Agent. |
Load |
Load<public>(Agent:agent):void |
Loads this device's saved data for Agent. |
ClearPersistenceData |
ClearPersistenceData<public>(Agent:agent):void |
Clears this device's saved data for Agent. |
ClearPersistenceDataForAll |
ClearPersistenceDataForAll<public>(Agent:agent):void |
Clears this device's saved data for all agents. |
ClearPersistenceDataForAll |
ClearPersistenceDataForAll<public>():void |
Clears this device's saved data for all agents. |
SetActiveDuration |
SetActiveDuration<public>(Time:float, Agent:agent):void |
Sets the remaining time (in seconds) on the timer, if active, on Agent. |
SetActiveDuration |
SetActiveDuration<public>(Time:float):void |
Sets the remaining time (in seconds) on the timer, if active. Use this function if the timer is set to use the same time for all agent's. |
GetActiveDuration |
GetActiveDuration<public>(Agent:agent)<transacts>:float |
Returns the remaining time (in seconds) on the timer for Agent. |
GetActiveDuration |
GetActiveDuration<public>()<transacts>:float |
Returns the remaining time (in seconds) on the timer if it is set to be global. |
SetLapTime |
SetLapTime<public>(Agent:agent):void |
Sets the lap time indicator for Agent. |
SetLapTimeForAll |
SetLapTimeForAll<public>(Agent:agent):void |
Sets the lap time indicator for all agents. |
SetLapTimeForAll |
SetLapTimeForAll<public>():void |
Sets the lap time indicator for all agents. |
SetMaxDuration |
SetMaxDuration<public>(Time:float):void |
Sets the maximum duration of the timer (in seconds). |
GetMaxDuration |
GetMaxDuration<public>()<transacts>:float |
Returns the maximum duration of the timer (in seconds). |
IsStatePerAgent |
IsStatePerAgent<public>()<transacts><decides>:void |
Succeeds if this device is tracking timer state for each individual agent independently. Fails if state is being tracked globally for all agent's. |
Walkthrough
The Scene
You have a sun-drenched pirate island. On the clifftop overlooking the lagoon sits Captain Crow, a cel-shaded NPC guard. A pressure-plate trigger_device is hidden under the dock planks. When a player steps on it, Captain Crow enters Alert state and a 15-second timer_device starts counting down. When the timer succeeds (time runs out), Crow returns to Idle. A second trigger fires a cannon-blast VFX to punctuate the alert.
The Verse Device
This is a creative_device (not the npc_behavior subclass itself — see Common Patterns for the behavior class). It lives in your level and wires the trigger + timer together, then calls methods on those devices to drive the state machine.
using { /Fortnite.com/Devices }
using { /Fortnite.com/AI }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
# Pirate lookout state machine — clifftop dock
# Attach this device to your level. Wire:
# DockTrigger -> a trigger_device under the dock planks
# AlertTimer -> a timer_device set to Count Down, 15 s
# CannonTrigger -> a trigger_device that fires cannon VFX
piratelookout_device := class(creative_device):
# The pressure plate hidden under the dock planks
@editable
DockTrigger : trigger_device = trigger_device{}
# 15-second countdown — configure in UEFN as Count Down, 15 s
@editable
AlertTimer : timer_device = timer_device{}
# Fires the cannon-blast VFX device when alert begins
@editable
CannonTrigger : trigger_device = trigger_device{}
# Track alert state so we don't double-start the timer
var IsAlerted : logic = false
OnBegin<override>()<suspends> : void =
# Start with the timer disabled — we enable it on alert
AlertTimer.Disable()
# Allow the dock trigger to fire unlimited times
DockTrigger.SetMaxTriggerCount(0)
# Subscribe: player steps on dock -> enter Alert state
DockTrigger.TriggeredEvent.Subscribe(OnDockTriggered)
# Subscribe: timer runs out -> return to Idle state
AlertTimer.SuccessEvent.Subscribe(OnAlertExpired)
# Subscribe: timer enters urgency mode -> flash cannon again
AlertTimer.StartUrgencyModeEvent.Subscribe(OnUrgencyMode)
# Called when a player steps on the dock trigger
OnDockTriggered(MaybeAgent : ?agent) : void =
if (IsAlerted?):
# Already alerted, ignore duplicate triggers
else:
set IsAlerted = true
# Fire the cannon VFX trigger (no agent needed)
CannonTrigger.Trigger()
# Enable and start the alert countdown
AlertTimer.Enable()
AlertTimer.Start()
# Called when the 15-second alert window expires (SuccessEvent)
OnAlertExpired(MaybeAgent : ?agent) : void =
set IsAlerted = false
# Stop and reset the timer, disable until next alert
AlertTimer.ResetForAll()
AlertTimer.Disable()
# Re-enable the dock trigger for the next patrol cycle
DockTrigger.Enable()
# Called when the timer enters urgency mode (last few seconds)
OnUrgencyMode(MaybeAgent : ?agent) : void =
# Fire cannon trigger again as a "last warning" visual
CannonTrigger.Trigger()```
### Line-by-Line Explanation
| Lines | What's happening |
|---|---|
| `@editable` fields | Expose `DockTrigger`, `AlertTimer`, and `CannonTrigger` to the UEFN editor so you can drag-assign placed devices. |
| `var IsAlerted` | A simple boolean gate — prevents re-entering Alert if the player keeps stepping on the trigger. |
| `AlertTimer.Disable()` | Timer starts disabled; we only enable it when the alert fires, preventing accidental auto-start. |
| `DockTrigger.SetMaxTriggerCount(0)` | `0` = unlimited triggers, so the patrol cycle can repeat forever. |
| `.Subscribe(OnDockTriggered)` | Hooks the `listenable(?agent)` event. The handler receives `?agent` (optional). |
| `CannonTrigger.Trigger()` | Fires the cannon VFX trigger with no agent — the no-arg overload is perfect here. |
| `AlertTimer.Enable()` then `AlertTimer.Start()` | Enable must come before Start or the timer ignores the call. |
| `AlertTimer.ResetForAll()` | Resets the countdown back to 15 s for all agents, ready for the next cycle. |
| `OnUrgencyMode` | `StartUrgencyModeEvent` fires when the timer enters its urgency window (configured in UEFN). We fire the cannon again as a dramatic "last warning". |
---
## Common Patterns
### Pattern 1 — Custom `npc_behavior` Subclass (the NPC's own brain)
This is the class you assign in the Character Definition asset. It runs *inside* the NPC and can use `focus_interface` to make Captain Crow stare at the lagoon.
```verse
using { /Fortnite.com/AI }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }
# Assign this class in the NPC's CharacterDefinition asset
captain_crow_behavior := class(npc_behavior):
# Called by the engine when the NPC spawns
OnBegin<override>()<suspends> : void =
# Get the fort_character for this NPC
if (Char := GetCharacter[]):
# Get the focus interface and make Crow stare at the lagoon
if (Focus := Char.GetFocusInterface[]):
# MaintainFocus never returns on its own — use race to cancel
race:
# Stare at a fixed lagoon landmark (world coords)
Focus.MaintainFocus(vector3{X := 12000.0, Y := -3400.0, Z := 200.0})
# After 8 seconds, break the focus (NPC resumes default)
block:
Sleep(8.0)
Key point: MaintainFocus has the <suspends> effect — it never returns unless canceled. Wrap it in a race block alongside a Sleep or another event so you can break out of it.
Pattern 2 — Timed Alert Window with SetActiveDuration
Dynamically shorten or lengthen the alert window at runtime — useful if the player is carrying a "wanted" item that makes guards more persistent.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Adjusts the alert timer duration based on a "heat level" variable
alert_duration_device := class(creative_device):
@editable
AlertTimer : timer_device = timer_device{}
@editable
HeatTrigger : trigger_device = trigger_device{}
# Heat level: 1 = normal (15 s), 2 = hot (30 s)
var HeatLevel : int = 1
OnBegin<override>()<suspends> : void =
HeatTrigger.TriggeredEvent.Subscribe(OnHeatTriggered)
AlertTimer.SuccessEvent.Subscribe(OnTimerDone)
OnHeatTriggered(MaybeAgent : ?agent) : void =
# Unwrap the optional agent
if (A := MaybeAgent?):
# High heat: give the player 30 s of alert time
set HeatLevel = 2
AlertTimer.SetActiveDuration(30.0, A)
AlertTimer.Start(A)
else:
# No agent (triggered by code): use default 15 s
AlertTimer.Start()
OnTimerDone(MaybeAgent : ?agent) : void =
set HeatLevel = 1
AlertTimer.ResetForAll()
Key point: SetActiveDuration(Time, Agent) takes a float and an agent — you must unwrap ?agent first. The no-agent Start() overload is used as a fallback when the trigger fires from code.
Pattern 3 — One-Shot Alarm with SetMaxTriggerCount and GetTriggerCountRemaining
The cannon alarm should only fire once per round. After it fires, disable the trigger and show remaining count in debug logic.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# One-shot alarm: fires once, then locks out until round reset
oneshot_alarm_device := class(creative_device):
@editable
AlarmTrigger : trigger_device = trigger_device{}
@editable
ResetTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
# Clamp to exactly 1 trigger per round
AlarmTrigger.SetMaxTriggerCount(1)
# Set a 2-second reset delay before it could theoretically re-fire
AlarmTrigger.SetResetDelay(2.0)
# Add a 0.5-second transmit delay before linked devices receive the signal
AlarmTrigger.SetTransmitDelay(0.5)
AlarmTrigger.TriggeredEvent.Subscribe(OnAlarmFired)
ResetTrigger.TriggeredEvent.Subscribe(OnRoundReset)
OnAlarmFired(MaybeAgent : ?agent) : void =
# Check how many triggers remain (0 = at limit)
Remaining := AlarmTrigger.GetTriggerCountRemaining()
if (Remaining = 0):
# Lock out the alarm for the rest of the round
AlarmTrigger.Disable()
OnRoundReset(MaybeAgent : ?agent) : void =
# Re-arm the alarm for the next round
AlarmTrigger.SetMaxTriggerCount(1)
AlarmTrigger.Enable()
Key point: GetTriggerCountRemaining() returns 0 when GetMaxTriggerCount() is unlimited and when the limit has been reached — check GetMaxTriggerCount() first if you need to distinguish the two cases.
Gotchas
1. npc_behavior has no events or methods of its own — override OnBegin
npc_behavior is an <abstract> class with no built-in events. Your entire NPC logic lives in OnBegin<override>()<suspends>:void. Don't try to call methods on npc_behavior directly from a creative_device — you can't. Use GetNPCBehavior() on an agent to retrieve the behavior instance, then cast it to your subclass.
2. MaintainFocus suspends forever — always wrap in race
focus_interface.MaintainFocus has <suspends> and never returns on its own. If you call it bare in OnBegin, your NPC's brain freezes at that line. Always pair it with a Sleep, an event await, or another race branch that can cancel it.
3. ?agent must be unwrapped before use
TriggeredEvent, SuccessEvent, and FailureEvent all deliver ?agent (optional). Calling .SetActiveDuration(Time, MaybeAgent?) directly won't compile. Unwrap first:
if (A := MaybeAgent?):
AlertTimer.SetActiveDuration(30.0, A)
verse
StateLabel<localizes>(S : string) : message = "{S}"
Then pass StateLabel("Alert"). There is no StringToMessage function.
7. int and float don't auto-convert
SetActiveDuration takes a float. Passing an int literal like 30 will fail — write 30.0 explicitly.
Get the complete code — free
You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.
Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.
Check your understanding
Test yourself with an interactive quiz and track your progress + earn XP — free for members.
Turn this into a guided course
Add Build a Verse-driven NPC state machine that patrols, alerts, and times out — all on a sun-drenched pirate island clifftop. to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.
Original tutorial generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.