Overview
The one-responsibility-per-function rule is not a device you place in the world — it's a coding discipline that keeps your Verse readable, testable, and easy to change. The idea: every function should do exactly one thing, and its name should say what that thing is. When a function called LaunchCrate also plays a sound, hides three props, and awards points, you can no longer trust its name, reuse it, or debug it.
The game problem it solves: UEFN scripts grow fast. A loot crate that reacts to a trigger might need to show itself, launch into the air, spin, and later hide. If you cram all of that into one giant handler, you can't reuse "launch" for the barrel next to it, and you can't test "hide" without triggering the whole chain. Splitting into small named functions fixes that.
We'll demonstrate on the creative_prop — a Fortnite prop placed or spawned in the island — because it exposes a clean set of single-purpose methods (Show, Hide, ApplyLinearImpulse, SetAngularVelocity, SetMesh) that map perfectly to one-job functions. Reach for this discipline the moment your OnBegin grows past a handful of lines.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: a floating loot crate. It starts hidden. When a player steps on a trigger plate, the crate appears, gets launched upward, and starts spinning so players notice it. Each of those is its own function — one responsibility each.
floot_crate_device := class(creative_device):
# The crate prop we control. Assign a placed prop in the Details panel.
@editable
Crate : creative_prop = creative_prop{}
# The plate players step on to reveal the crate.
@editable
RevealPlate : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
# One responsibility: initial setup — start the crate hidden.
HideCrate()
# Wire the plate to our reveal sequence.
RevealPlate.TriggeredEvent.Subscribe(OnPlateStepped)
# ONE job: hide the crate.
HideCrate() : void =
Crate.Hide()
# ONE job: show the crate.
ShowCrate() : void =
Crate.Show()
# ONE job: launch the crate straight up with a physics impulse.
LaunchCrate() : void =
UpImpulse := vector3{X := 0.0, Y := 0.0, Z := 1500.0}
Crate.ApplyLinearImpulse(UpImpulse)
# ONE job: make the crate spin around the vertical axis.
SpinCrate() : void =
Spin := vector3{X := 0.0, Y := 0.0, Z := 6.0}
Crate.SetAngularVelocity(Spin)
# The event handler ORCHESTRATES the single-job functions.
# It reads top-to-bottom like a checklist.
OnPlateStepped(Agent : ?agent) : void =
ShowCrate()
LaunchCrate()
SpinCrate()
Line by line:
@editable Crate : creative_prop = creative_prop{}— the prop field. You must declare a device/prop as an@editablefield to call its methods; a bareSomeProp.Show()fails with 'Unknown identifier'. Assign a placed prop to it in UEFN's Details panel.@editable RevealPlate : trigger_device— the plate that starts the sequence.OnBegindoes two things, but each is a single call to a single-job function or subscription — it's an orchestrator, not a worker. It callsHideCrate()and subscribesOnPlateSteppedto the plate'sTriggeredEvent.HideCrate/ShowCrateeach wrap exactly one prop call. Trivial? Yes — and that's the point. The name documents intent, and you can callHideCrate()from anywhere without duplicatingCrate.Hide().LaunchCratebuilds onevector3(Newton·seconds) and applies it viaApplyLinearImpulse. Its only job is the launch.SpinCratesets angular velocity (radians/second) — one job.OnPlateSteppedis the orchestrator. It doesn't do the work; it composes the small functions in order. Notice how readable it is: show, launch, spin.
Because each behavior is isolated, you can reuse LaunchCrate for a barrel, or test SpinCrate alone, without touching anything else.
Common patterns
Pattern 1 — A reset function that reuses your hide-job. Because HideCrate already exists, the reset doesn't duplicate it.
crate_reset_device := class(creative_device):
@editable
Crate : creative_prop = creative_prop{}
@editable
ResetButton : button_device = button_device{}
OnBegin<override>()<suspends> : void =
ResetButton.InteractedWithEvent.Subscribe(OnResetPressed)
# ONE job: stop all motion on the crate.
StopCrate() : void =
Zero := vector3{X := 0.0, Y := 0.0, Z := 0.0}
Crate.SetLinearVelocity(Zero)
Crate.SetAngularVelocity(Zero)
# ONE job: hide the crate.
HideCrate() : void =
Crate.Hide()
# Orchestrator composes the two single-job functions.
OnResetPressed(Agent : agent) : void =
StopCrate()
HideCrate()
Pattern 2 — A swap-appearance function isolates the visual change. SetMesh is its own responsibility; the handler stays a thin composer.
crate_skin_device := class(creative_device):
@editable
Crate : creative_prop = creative_prop{}
@editable
NewLook : mesh = mesh{}
@editable
SwapPlate : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
SwapPlate.TriggeredEvent.Subscribe(OnSwap)
# ONE job: change the crate's mesh.
ReskinCrate() : void =
if (Crate.IsValid[]):
Crate.SetMesh(NewLook)
OnSwap(Agent : ?agent) : void =
ReskinCrate()
Pattern 3 — A cleanup function whose single job is disposal. Guard with IsValid so you never dispose twice.
crate_cleanup_device := class(creative_device):
@editable
Crate : creative_prop = creative_prop{}
@editable
RemoveButton : button_device = button_device{}
OnBegin<override>()<suspends> : void =
RemoveButton.InteractedWithEvent.Subscribe(OnRemove)
# ONE job: safely dispose the crate.
RemoveCrate() : void =
if (Crate.IsValid[]):
Crate.Dispose()
OnRemove(Agent : agent) : void =
RemoveCrate()
Gotchas
- Orchestrators still count as single-responsibility.
OnPlateSteppedcalls three functions — that's fine, because its one job is "react to the plate by running the reveal sequence." The rule bans mixing implementation details (building vectors, calling APIs) with high-level flow, not calling multiple named steps. - You must declare props as
@editablefields.creative_prop{}as a default is a placeholder; assign the real placed prop in the Details panel or method calls silently target nothing. IsValidandIsDisposedare<decides>— use[]brackets. Writeif (Crate.IsValid[]):, notCrate.IsValid(). GuardDisposeandSetMeshwith a validity check so you don't operate on a removed prop.- Physics methods do nothing if physics is off.
ApplyLinearImpulse,SetLinearVelocity, andSetAngularVelocityrequire the prop to have physics enabled in its settings — otherwise yourLaunchCratefunction is correct but silently inert. - Verse never auto-converts int↔float. Vector components are floats: write
Z := 1500.0, notZ := 1500. A bare int here is a compile error. - A
listenable(?agent)handler receives?agent. TheTriggeredEventgives you(Agent : ?agent); unwrap withif (A := Agent?):before using the player. ButtonInteractedWithEventhands a plainagent. Keep the signatures right or subscription fails to compile. - Don't over-split. One-responsibility doesn't mean one-line-per-function forever. If a wrapper adds no clarity and is called once, inline it. The goal is readability, not ceremony.