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.

Coral the flamingo has seen a thousand first islands, and they all hit the same sandbar: one OnBegin (or one event handler) that keeps growing until nobody — including the person who wrote it — can say what any one part does. The fix is the very first habit of professional code: every function gets exactly one job. Today you take the tangled shell-hunt handler you have been building across South Shores and split it into three small, named, single-purpose functions: CollectShell(), UpdateHud(), and CheckWin().

What you will build

This is the refactor that turns one giant handler into the clean function set the Shell Hunt capstone runs on. You already have all the ingredients from earlier South Shores lessons: trigger_device zones and their ?agent payload (Invisible Tripwires), hud_message_device text (Talk to Players), var/set counters (Keeping Score), and "Shells: {Count}/{Total}" interpolation (Say It With Braces). What is new here is not an API — it is structure. By the end you will have a device where:

  • CollectShell(Agent) records a pickup — and nothing else,
  • UpdateHud() paints the count on screen — and nothing else,
  • CheckWin(Agent) decides whether the round is over — and nothing else.

Three names, three jobs, zero tangles. This is the first SOLID habit (the "S" — single responsibility), and every later zone assumes you have it.

Walkthrough

Step 1 — Meet the blob

Here is the shell hunt the way most first drafts look. It compiles. It even works. It is also where good islands go to sink:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

# The "before" picture: one handler doing every job on the beach.
messy_hunt_device := class(creative_device):

    @editable
    ShellTrigger : trigger_device = trigger_device{}

    @editable
    HuntHUD : hud_message_device = hud_message_device{}

    @editable
    EndGame : end_game_device = end_game_device{}

    var ShellsFound : int = 0

    CountText<localizes>(Found : string) : message = "Shells: {Found}/3"
    WinText<localizes>() : message = "All shells found — you win!"

    OnBegin<override>()<suspends> : void =
        ShellTrigger.TriggeredEvent.Subscribe(OnShellTouched)
        Sleep(Inf)

    # One handler, THREE jobs: counting, HUD text, and win logic — all tangled.
    OnShellTouched(Agent : ?agent) : void =
        set ShellsFound += 1
        FoundStr : string = "{ShellsFound}"
        HuntHUD.SetText(CountText(FoundStr))
        HuntHUD.SetDisplayTime(0.0)
        HuntHUD.Show()
        if (ShellsFound >= 3):
            HuntHUD.SetText(WinText())
            HuntHUD.Show()
            if (Winner := Agent?):
                EndGame.Activate(Winner)

Read OnShellTouched out loud: "add one to the counter AND build the HUD string AND set the text AND show it AND check the win AND maybe end the game." Every AND is a separate job hiding in one function. Want to reuse the HUD update when the round starts? You cannot — it is welded to the counting. Want to test the win check on its own? Same problem.

Step 2 — Find the jobs

Before touching code, name the responsibilities. In the blob there are exactly three:

Job Lines that do it Future function
Record a pickup set ShellsFound += 1 CollectShell()
Show the count FoundStr…, SetText, SetDisplayTime, Show UpdateHud()
Decide the win if (ShellsFound >= 3)…, EndGame.Activate CheckWin()

A good smell test: if describing a function honestly requires the word "and", it has more than one job.

Step 3 — The refactor

Same behaviour, new shape. This is the complete device — drop it in, wire the three triggers, the HUD device, and the End Game device in the Details panel:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

# The "after" picture: the same shell hunt, split into one function per job.
shell_hunt_device := class(creative_device):

    # Three shell pickup zones on the beach — wire each in the Details panel.
    @editable
    ShellTriggerA : trigger_device = trigger_device{}

    @editable
    ShellTriggerB : trigger_device = trigger_device{}

    @editable
    ShellTriggerC : trigger_device = trigger_device{}

    # The HUD message device that shows the live count.
    @editable
    HuntHUD : hud_message_device = hud_message_device{}

    # Ends the round when every shell is found.
    @editable
    EndGame : end_game_device = end_game_device{}

    # How many shells are hidden this round.
    @editable
    TotalShells : int = 3

    var ShellsFound : int = 0

    # Verse HUD text must be a `message`; a <localizes> function is the bridge.
    ShellCountText<localizes>(Found : string, Total : string) : message =
        "Shells: {Found}/{Total}"

    WinnerText<localizes>() : message =
        "All shells found — the hunt is won!"

    OnBegin<override>()<suspends> : void =
        # Every shell zone reports to the SAME function.
        ShellTriggerA.TriggeredEvent.Subscribe(CollectShell)
        ShellTriggerB.TriggeredEvent.Subscribe(CollectShell)
        ShellTriggerC.TriggeredEvent.Subscribe(CollectShell)
        UpdateHud()
        Sleep(Inf)

    # Job 1 — record one collected shell, then hand off. Nothing else.
    CollectShell(Agent : ?agent) : void =
        set ShellsFound += 1
        UpdateHud()
        CheckWin(Agent)

    # Job 2 — push the current count to the HUD. Nothing else.
    UpdateHud() : void =
        FoundStr : string = "{ShellsFound}"
        TotalStr : string = "{TotalShells}"
        HuntHUD.SetText(ShellCountText(FoundStr, TotalStr))
        HuntHUD.SetDisplayTime(0.0)
        HuntHUD.Show()

    # A small PURE function with a return value: same inputs, same answer,
    # no side effects. Easy to read, easy to reuse, easy to trust.
    IsHuntComplete(Found : int, Total : int)<transacts> : logic =
        if (Found >= Total) { true } else { false }

    # Job 3 — decide whether the hunt is over, and end the round if so.
    CheckWin(Agent : ?agent) : void =
        if (IsHuntComplete(ShellsFound, TotalShells)?):
            HuntHUD.SetText(WinnerText())
            HuntHUD.Show()
            if (Winner := Agent?):
                EndGame.Activate(Winner)

What changed, line by line

Piece What it does now
CollectShell(Agent : ?agent) The only place the counter changes. It does its one job, then delegates: UpdateHud() and CheckWin(Agent).
UpdateHud() The only place HUD text is built and shown. OnBegin now reuses it to paint Shells: 0/3 at round start — for free, because it is no longer welded to the counting.
IsHuntComplete(Found, Total) : logic A pure function with parameters and a return value. It reads no device state and touches no screen — give it two ints, get back a logic. Note the ? query when calling it: if (IsHuntComplete(...)?).
CheckWin(Agent : ?agent) The only place the round can end. It asks IsHuntComplete for the verdict, then unwraps the optional agent (if (Winner := Agent?)) before EndGame.Activate(Winner) — the same ?agent unwrap you learned in Invisible Tripwires.
TotalShells as @editable The win condition is no longer a magic 3 buried in an if. Designers can retune the hunt without opening Verse.

Step 4 — Wire it in the editor

  1. Place three Trigger devices on your shell spots, a HUD Message device, and an End Game device.
  2. Select your shell_hunt_device, and in the Details panel assign ShellTriggerA/B/C, HuntHUD, and EndGame.
  3. Launch Session, step on the zones, and watch the count climb to the winner banner.

Common patterns

Pattern 1 — Parameters over hidden state

UpdateHud() above reads the class counter directly, which is fine inside one device. But the moment a helper takes its inputs as parameters, it stops caring where the numbers came from — and becomes reusable anywhere:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

hud_helper_device := class(creative_device):

    @editable
    HuntHUD : hud_message_device = hud_message_device{}

    CountText<localizes>(Found : string, Total : string) : message =
        "Shells: {Found}/{Total}"

    # Parameters instead of hidden state: this helper can show ANY count,
    # for any game, without knowing where the numbers came from.
    ShowCount(Found : int, Total : int) : void =
        FoundStr : string = "{Found}"
        TotalStr : string = "{Total}"
        HuntHUD.SetText(CountText(FoundStr, TotalStr))
        HuntHUD.SetDisplayTime(0.0)
        HuntHUD.Show()

    OnBegin<override>()<suspends> : void =
        ShowCount(0, 3)
        Sleep(2.0)
        ShowCount(1, 3)

This parameterised ShowCount(Found, Total) is exactly the shape that graduates into the ShellHuntKit module's HUD helper two lessons from now.

Pattern 2 — Return values make decisions testable

Functions that compute an answer and hand it back (instead of acting on the world) are the easiest code you will ever debug — same inputs, same output, every time:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

bonus_score_device := class(creative_device):

    # Pure function: rarer shells (3rd and beyond) are worth more.
    ScoreForShell(ShellNumber : int) : int =
        if (ShellNumber >= 3) { 25 } else { 10 }

    IsHuntComplete(Found : int, Total : int)<transacts> : logic =
        if (Found >= Total) { true } else { false }

    OnBegin<override>()<suspends> : void =
        var TotalScore : int = 0
        for (N := 1..3):
            set TotalScore += ScoreForShell(N)
        Print("Final score: {TotalScore}")
        if (IsHuntComplete(3, 3)?):
            Print("Hunt complete!")

ScoreForShell and IsHuntComplete never touch a device. That means you can reason about them in your head, reuse them in any project, and — when something goes wrong at 2 a.m. — trust them completely while you hunt elsewhere.

Where this goes next

This clean function set is the skeleton of the Shell Hunt capstone — the beach minigame South Shores has been building toward. From here, each remaining lesson upgrades one function without disturbing the others:

  • One Handler, Many Devices (next lesson) replaces the three copy-pasted Subscribe lines by fanning an array of ~10 shell triggers into your single CollectShell — possible only because CollectShell is already one clean, shared function.
  • Your First Module: the ShellHuntKit Toolbox lifts UpdateHud's logic into a reusable module (the zone's export that later zones import with using).
  • The capstone's spawning, bobbing, and teleport features each slot in as new small functions beside these three — never inside them.

One job each. Coral insists — and so will every teammate you ever have.

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 →