Reference Verse compiles

Clean Functions: One Job Each

Every first island hits the same sandbar: one OnBegin or event handler that grows until nobody can say what any part of it does. This lesson teaches the first habit of professional Verse: every function gets exactly one job. You will take the tangled shell-hunt handler from earlier South Shores lessons and extract CollectShell(), UpdateHud(), and CheckWin() as small single-purpose functions with parameters and return values — the exact clean function set the Shell Hunt capstone is built on.

Updated Examples verified on the live UEFN compiler
Watch the Knotone_responsibility_per_function in ~90 seconds.

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 @editable field to call its methods; a bare SomeProp.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.
  • OnBegin does two things, but each is a single call to a single-job function or subscription — it's an orchestrator, not a worker. It calls HideCrate() and subscribes OnPlateStepped to the plate's TriggeredEvent.
  • HideCrate / ShowCrate each wrap exactly one prop call. Trivial? Yes — and that's the point. The name documents intent, and you can call HideCrate() from anywhere without duplicating Crate.Hide().
  • LaunchCrate builds one vector3 (Newton·seconds) and applies it via ApplyLinearImpulse. Its only job is the launch.
  • SpinCrate sets angular velocity (radians/second) — one job.
  • OnPlateStepped is 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. OnPlateStepped calls 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 @editable fields. creative_prop{} as a default is a placeholder; assign the real placed prop in the Details panel or method calls silently target nothing.
  • IsValid and IsDisposed are <decides> — use [] brackets. Write if (Crate.IsValid[]):, not Crate.IsValid(). Guard Dispose and SetMesh with a validity check so you don't operate on a removed prop.
  • Physics methods do nothing if physics is off. ApplyLinearImpulse, SetLinearVelocity, and SetAngularVelocity require the prop to have physics enabled in its settings — otherwise your LaunchCrate function is correct but silently inert.
  • Verse never auto-converts int↔float. Vector components are floats: write Z := 1500.0, not Z := 1500. A bare int here is a compile error.
  • A listenable(?agent) handler receives ?agent. The TriggeredEvent gives you (Agent : ?agent); unwrap with if (A := Agent?): before using the player. Button InteractedWithEvent hands a plain agent. 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.

Build your own lesson with one_responsibility_per_function

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →