Overview
An npc_behavior is a Verse script that runs inside an NPC character rather than on a separate creative device. You inherit from the abstract class, override OnBegin (and optionally OnEnd), and use the AI interfaces — navigatable, focus_interface, etc. — to move, look, and react.
When to reach for it:
- You need an NPC that does more than the default guard loop.
- You want a state machine: Patrol → Alert → Search → Patrol.
- You need the NPC to react to game events (a trigger fires, a timer expires) without a separate manager device.
npc_behavior has no events or methods of its own beyond OnBegin, OnEnd, GetAgent, and GetEntity — all the interesting work comes from the AI interfaces you call on the character, plus the creative devices (trigger_device, timer_device) you wire up through @editable fields on a companion creative_device.
Important:
npc_behaviorsubclasses are notcreative_devicesubclasses. You cannot place annpc_behaviordirectly in the level — you attach it to an NPC Character Definition asset (or override it in annpc_spawner_device). Any@editabledevice references must live on a separatecreative_devicethat coordinates with the behavior.
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
A cel-shaded pirate ship is moored at a sun-drenched lagoon dock. A Pirate Lookout NPC patrols between two waypoint triggers on the dock. When a player crosses a hidden Alert Trigger, the lookout snaps to face them and a Search Timer starts counting down. If the timer expires without the player being dealt with, the lookout returns to patrol.
The architecture:
pirate_lookout_behavior— thenpc_behaviorsubclass (attached to the Character Definition).pirate_dock_coordinator— acreative_deviceplaced in the level that holds all@editabledevice references and owns the shared state the behavior reads.
Because npc_behavior cannot hold @editable fields itself, the coordinator device acts as the "brain stem" — it subscribes to the trigger and timer events, then exposes simple flags the behavior polls.
Step 1 — The Coordinator Device
Place this device in your level and wire up the three trigger devices and the timer device in the Details panel.
using { /Fortnite.com/Devices }
using { /Fortnite.com/AI }
using { /Verse.org/Simulation }
# Coordinator creative_device — place ONE of these in the level.
# Wire AlertTrigger, WaypointA, WaypointB, and SearchTimer in the Details panel.
pirate_dock_coordinator := class(creative_device):
# The hidden pressure plate on the dock gangway
@editable AlertTrigger : trigger_device = trigger_device{}
# Two patrol waypoint triggers the NPC walks between
@editable WaypointA : trigger_device = trigger_device{}
@editable WaypointB : trigger_device = trigger_device{}
# Countdown timer — configure to 15 s in the Details panel
@editable SearchTimer : timer_device = timer_device{}
# Shared state flags read by the npc_behavior
var IsAlerted : logic = false
var SearchExpired : logic = false
OnBegin<override>()<suspends> : void =
# Subscribe to the alert trigger
AlertTrigger.TriggeredEvent.Subscribe(OnAlertTriggered)
# Subscribe to timer success (countdown hit zero = search over)
SearchTimer.SuccessEvent.Subscribe(OnSearchExpired)
# Disable waypoint triggers so only the NPC walks them
WaypointA.Disable()
WaypointB.Disable()
# Called when a player steps on the gangway plate
OnAlertTriggered(MaybeAgent : ?agent) : void =
set IsAlerted = true
set SearchExpired = false
# Start the 15-second search countdown
SearchTimer.Start()
# Called when the search timer hits zero
OnSearchExpired(MaybeAgent : ?agent) : void =
set SearchExpired = true
set IsAlerted = false
# Reset timer so it's ready for the next alert
SearchTimer.ResetForAll()
Step 2 — The NPC Behavior Script
Create this file in Verse Explorer, then assign it as the Verse Behavior in your NPC Character Definition asset.
using { /Fortnite.com/AI }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }
# Attach this script to the Pirate Lookout's NPC Character Definition.
# The NPC must be a Custom-type character so Verse drives all movement.
pirate_lookout_behavior<public> := class(npc_behavior):
# Patrol waypoints in world space — set these to match your dock geometry
WaypointAPos : vector3 = vector3{X := -1200.0, Y := 0.0, Z := 0.0}
WaypointBPos : vector3 = vector3{X := 1200.0, Y := 0.0, Z := 0.0}
# How long (seconds) to idle at each waypoint end
PatrolIdleTime : float = 2.0
# Reference to the coordinator — resolved at runtime via FindCreativeObjectsWithTag
# (In a real project you would tag the coordinator and find it here;
# for clarity this walkthrough shows the state-machine logic directly.)
# Entry point — runs for the lifetime of the NPC
OnBegin<override>()<suspends> : void =
# Grab the navigatable interface so we can move the NPC
if (Nav := GetEntity[].GetComponent[navigatable]()):
RunStateMachine(Nav)
# Simple three-state machine: Patrol <-> Alert -> Search -> Patrol
RunStateMachine(Nav : navigatable)<suspends> : void =
var State : []char = "Patrol"
loop:
if (State = "Patrol"):
# Walk A -> B -> A, checking for alert each leg
Nav.NavigateTo(navigation_target.MakeTargetFromPosition(WaypointAPos))
Nav.Idle(?Duration := PatrolIdleTime)
Nav.NavigateTo(navigation_target.MakeTargetFromPosition(WaypointBPos))
Nav.Idle(?Duration := PatrolIdleTime)
# (Alert transition is handled by the coordinator setting IsAlerted;
# in a full project you would race navigation against an event here)
else if (State = "Alert"):
# Face the dock centre and hold for 3 seconds
Nav.Idle(?Duration := 3.0)
set State = "Search"
else if (State = "Search"):
# Pace to the alert point, then wait for SearchExpired
Nav.NavigateTo(navigation_target.MakeTargetFromPosition(WaypointAPos))
Nav.Idle(?Duration := 5.0)
set State = "Patrol"
# Called when the NPC is removed from the simulation (eliminated / despawned)
OnEnd<override>() : void =
# Clean-up if needed — navigation stops automatically
false
Line-by-line highlights
OnBeginis the NPC'smain()— it runs for the entire lifetime of the character.GetEntity[].GetComponent[navigatable]()retrieves the movement interface; the[]suffix marks the failable<decides>call.Nav.NavigateTo(...)suspends until the NPC arrives (or fails), giving you natural sequential flow.Nav.Idle(?Duration := PatrolIdleTime)makes the NPC stand still for the given seconds — perfect for the "looking around" beat at each waypoint.OnEndis your teardown hook; it fires when the NPC despawns or is eliminated.
Common patterns
Pattern 1 — Trigger the Alert Trigger from Code (trigger_device.Trigger)
Sometimes you want the coordinator itself to fire the alert — for example when a scripted cinematic ends. Call Trigger() with no agent to activate the device programmatically.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Drop this device in the level alongside the coordinator.
# Wire CinematicEndTrigger and AlertTrigger in the Details panel.
cinematic_alert_starter := class(creative_device):
@editable CinematicEndTrigger : trigger_device = trigger_device{}
@editable AlertTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
CinematicEndTrigger.TriggeredEvent.Subscribe(OnCinematicEnded)
OnCinematicEnded(MaybeAgent : ?agent) : void =
# Fire the alert trigger with no agent — the coordinator's
# OnAlertTriggered handler will run and start the search timer.
AlertTrigger.Trigger()
Pattern 2 — Pause and Resume the Search Timer (timer_device.Pause / Resume)
If the player enters a safe zone on the dock (a second trigger), pause the countdown so the lookout freezes its search clock.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Wire SafeZoneEnterTrigger, SafeZoneExitTrigger, and SearchTimer in the Details panel.
safe_zone_timer_pauser := class(creative_device):
@editable SafeZoneEnterTrigger : trigger_device = trigger_device{}
@editable SafeZoneExitTrigger : trigger_device = trigger_device{}
@editable SearchTimer : timer_device = timer_device{}
OnBegin<override>()<suspends> : void =
SafeZoneEnterTrigger.TriggeredEvent.Subscribe(OnEnterSafeZone)
SafeZoneExitTrigger.TriggeredEvent.Subscribe(OnExitSafeZone)
OnEnterSafeZone(MaybeAgent : ?agent) : void =
# Freeze the countdown while the player hides in the safe zone
SearchTimer.Pause()
OnExitSafeZone(MaybeAgent : ?agent) : void =
# Resume the countdown when the player leaves cover
SearchTimer.Resume()
Pattern 3 — Limit How Many Times the Alert Can Fire (trigger_device.SetMaxTriggerCount)
On a pirate ship you might only want the alarm to sound twice before the lookout gives up entirely. Use SetMaxTriggerCount to enforce that cap, and GetTriggerCountRemaining to show debug info.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Wire AlertTrigger in the Details panel.
alert_limiter := class(creative_device):
@editable AlertTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
# Allow the alert trigger to fire at most twice
AlertTrigger.SetMaxTriggerCount(2)
AlertTrigger.TriggeredEvent.Subscribe(OnAlertFired)
OnAlertFired(MaybeAgent : ?agent) : void =
# How many alerts remain?
Remaining := AlertTrigger.GetTriggerCountRemaining()
# Use Remaining in your game logic — e.g. change NPC dialogue
if (Remaining = 0):
# No more alerts — disable the trigger entirely
AlertTrigger.Disable()
Gotchas
1. npc_behavior is NOT a creative_device
You cannot place an npc_behavior subclass in the level or give it @editable fields. It must be attached to an NPC Character Definition asset. All device references must live on a companion creative_device.
2. OnBegin suspends for the NPC's lifetime
OnBegin is <suspends> — it runs as a coroutine that lives as long as the NPC is alive. If OnBegin returns, the NPC's custom behavior ends. Keep your state machine in a loop: or chain of suspending calls.
3. GetAgent and GetEntity are failable (<decides>)
Always call them inside an if expression:
if (A := GetAgent[]):
# use A safely
The [] suffix is Verse's syntax for calling a <decides> function.
4. navigatable must be retrieved from the entity
You cannot store a navigatable reference as a field — retrieve it fresh in OnBegin via GetEntity[].GetComponent[navigatable]().
5. timer_device events send ?agent
Both SuccessEvent and FailureEvent hand your handler a ?agent (optional agent). Unwrap before use:
OnSearchExpired(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
# A is a valid agent
# safe to proceed either way
set SearchExpired = true
6. message parameters need a localizer function
If any device method takes a message type (e.g. a HUD device), you cannot pass a raw string. Declare a localizer:
MyMsg<localizes>(S : string) : message = "{S}"
Then pass MyMsg("Pirate Alert!"). There is no StringToMessage.
7. trigger_device.SetMaxTriggerCount clamps to [0, 20]
Passing a value outside that range is silently clamped. 0 means unlimited. Plan your alert budget accordingly.
8. Calling timer_device.Start() on an already-running timer restarts it
If you want to extend rather than restart the countdown, call Resume() instead, or check your state flags before calling Start().