Coral the flamingo has been pacing the boardwalk all week, and today she finally gets to say it: ship day. Every lesson on South Shores built one piece of this game. The Start button from lesson 2, the HUD from lesson 3, the shell triggers from lesson 4, the counter from lesson 5, your ShellHuntKit module from lesson 9, runtime shells from lesson 10, the teleport-home from lesson 12, the phase machine from lesson 14, the golden shell from lessons 15-16 — today they snap together into one playable round loop you can hand to a friend.
What you will build
The complete Shell-Hunt minigame: a repeatable round loop on the beached galleon.
The round loop:
- LOBBY — a welcome banner invites players; the START button on the galleon deck is armed.
- HUNTING — shells spawn at every shell spot, the timer starts, and every pickup updates the live
Shells: {n}/{Total}counter. The rare golden shell pays 3 points and grants a speed boost. - CELEBRATION — when the timer ends (or the beach is swept clean early), the winner is declared, the leftover shells are cleaned up, everyone teleports back to the galleon start line, and the game returns to LOBBY — ready to run again.
Architecture at a glance — one shell_hunt_game device orchestrates everything; one imported module supplies the shared helpers:
| Piece | Built in | Job in the capstone |
|---|---|---|
shell_hunt_kit module |
Lesson 9 (Your First Module) | All HUD messages + the UpdateShellHud helper — imported with using |
button_device |
Lesson 2 | The START button that kicks off a round |
hud_message_device |
Lessons 3 + 6 | Welcome banner, live counter, golden-shell flash, winner announcement |
[]trigger_device fan-in |
Lessons 4 + 8 | ~10 shell spots wired to ONE CollectShell path |
var counter + [player]int map |
Lesson 5 | Round count + per-hunter scores |
| Small single-job functions | Lesson 7 | CollectShell / AddScore / CheckWin / EndRound |
SpawnProp + Dispose |
Lesson 10 | Shells appear at round start, vanish at round end |
TeleportTo |
Lesson 12 | Everyone snaps back to the galleon after the win |
shell_hunt_phase enum |
Lesson 14 | LOBBY → HUNTING → CELEBRATION gates every handler |
Golden shell + movement_modulator_device |
Lessons 15 + 16 | The 3-point bonus and its speed-boost reward |
The ShellHuntKit import — your first cross-file reuse
This capstone imports the module you wrote in lesson 9. In your project, shell_hunt_kit lives in its own ShellHuntKit.verse file; the game device pulls it in with a single line:
using { shell_hunt_kit }
That one line is the whole point of modules: the game device below never builds a HUD string itself — it calls UpdateShellHud(...) and ShellCountMsg(...) from the kit. When East Volcano's capstone wants a countdown HUD later, it imports the same kit instead of rewriting it. (The listing below includes the module in-file so the entire capstone is one paste-and-compile unit — the using line works identically either way.)
Walkthrough
Place the device on the galleon deck, wire the devices listed in the checklist below, and paste this in. Read the flow top to bottom: it is literally the round loop.
using { /Fortnite.com/Characters }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Simulation }
# ─── ShellHuntKit: the module YOU built in "Your First Module" (lesson 9). ───
# In your project this lives in its own ShellHuntKit.verse file; the
# `using { shell_hunt_kit }` line below imports it exactly the same way
# whether it sits in this file or in that one. This is your first taste
# of cross-file imports: the game device below never defines a HUD string
# or a HUD call of its own — it borrows everything from the kit.
shell_hunt_kit := module:
# Every string a player ever sees lives here (lesson 6: interpolation).
WelcomeMsg<public><localizes>:message = "Press START on the galleon deck to begin the Shell-Hunt!"
GoldenMsg<public><localizes>:message = "GOLDEN SHELL! +3 points and a speed boost!"
ShellCountMsg<public><localizes>(Count:int, Total:int):message = "Shells: {Count}/{Total}"
WinnerMsg<public><localizes>(Score:int):message = "Round over! Top hunter scored {Score} points. Back to the galleon!"
# Shared HUD helper (lesson 3): one call sets the text AND shows it.
UpdateShellHud<public>(Hud:hud_message_device, Count:int, Total:int):void =
Hud.SetText(ShellCountMsg(Count, Total))
Hud.Show()
# Import our own module — the capstone's cross-file moment.
using { shell_hunt_kit }
# The phase machine from "Game Phases" (lesson 14).
shell_hunt_phase := enum{Lobby, Hunting, Celebration}
# ─── The whole game, in one device on the beached galleon. ───
shell_hunt_game := class(creative_device):
# ── Devices wired in the Details panel ──
@editable
StartButton : button_device = button_device{} # lesson 2
@editable
HuntHud : hud_message_device = hud_message_device{} # lesson 3
@editable
ShellTriggers : []trigger_device = array{} # lessons 4 + 8
@editable
GoldenTrigger : trigger_device = trigger_device{} # lesson 15's star pickup
@editable
RoundTimer : timer_device = timer_device{} # ends the round
@editable
SpeedBoost : movement_modulator_device = movement_modulator_device{} # lesson 16 reward
@editable
ShellAsset : creative_prop_asset = DefaultCreativePropAsset # lesson 10
# The galleon start line hunters return to (lesson 12).
# X=forward, Y=right, Z=up, in Unreal centimetres — match your island.
HomePosition : vector3 = vector3{X := 0.0, Y := 0.0, Z := 300.0}
# ── Round state (lessons 5 + 14) ──
var Phase : shell_hunt_phase = shell_hunt_phase.Lobby
var ShellsFound : int = 0
var Scores : [player]int = map{}
var SpawnedShells : []creative_prop = array{}
OnBegin<override>()<suspends>:void =
# One handler, many devices (lesson 8): fan-in every shell trigger.
for (ShellTrigger : ShellTriggers):
ShellTrigger.TriggeredEvent.Subscribe(OnShellTouched)
GoldenTrigger.TriggeredEvent.Subscribe(OnGoldenTouched)
StartButton.InteractedWithEvent.Subscribe(OnStartPressed)
RoundTimer.SuccessEvent.Subscribe(OnTimeUp)
HuntHud.SetText(WelcomeMsg)
HuntHud.Show()
# ── Phase: Lobby -> Hunting ──
OnStartPressed(Presser : agent):void =
# The phase gate: pressing START mid-hunt does nothing.
if (Phase = shell_hunt_phase.Lobby):
StartRound()
StartRound():void =
set Phase = shell_hunt_phase.Hunting
set ShellsFound = 0
set Scores = map{}
# Re-arm every shell spot for the new round (replayability!).
for (ShellTrigger : ShellTriggers):
ShellTrigger.Reset()
ShellTrigger.Enable()
GoldenTrigger.Reset()
GoldenTrigger.Enable()
SpawnShellProps()
RoundTimer.StartForAll()
UpdateShellHud(HuntHud, 0, ShellTriggers.Length)
# Runtime prop spawning (lesson 10): one shell prop above each spot.
SpawnShellProps():void =
SpawnRot := MakeRotation(vector3{X := 0.0, Y := 0.0, Z := 1.0}, 0.0)
for (ShellTrigger : ShellTriggers):
SpawnPoint := ShellTrigger.GetTransform().Translation + vector3{X := 0.0, Y := 0.0, Z := 50.0}
SpawnResult := SpawnProp(ShellAsset, SpawnPoint, SpawnRot)
if (Shell := SpawnResult(0)?):
set SpawnedShells += array{Shell}
# ── Collection: TriggeredEvent hands us ?agent (lesson 4) ──
OnShellTouched(MaybeAgent : ?agent):void =
# Phase gate AND optional unwrap in one if (both must succeed).
if (Phase = shell_hunt_phase.Hunting, Hunter := MaybeAgent?):
CollectShell(Hunter)
OnGoldenTouched(MaybeAgent : ?agent):void =
if (Phase = shell_hunt_phase.Hunting, Hunter := MaybeAgent?):
AddScore(Hunter, 3) # lesson 15: worth 3
SpeedBoost.Activate(Hunter) # lesson 16: the buff
HuntHud.Show(GoldenMsg, ?DisplayTime := 3.0)
# One job each (lesson 7): count, score, HUD, win check.
CollectShell(Hunter : agent):void =
set ShellsFound += 1
AddScore(Hunter, 1)
UpdateShellHud(HuntHud, ShellsFound, ShellTriggers.Length)
CheckWin()
AddScore(Hunter : agent, Points : int):void =
if (HunterPlayer := player[Hunter]):
var Current : int = 0
if (Existing := Scores[HunterPlayer]):
set Current = Existing
if (set Scores[HunterPlayer] = Current + Points) {}
CheckWin():void =
# Beach swept clean before the timer? End the round early.
if (ShellsFound >= ShellTriggers.Length):
spawn { EndRound() }
OnTimeUp(MaybeAgent : ?agent):void =
# EndRound suspends (it Sleeps), but handlers can't — so spawn it.
spawn { EndRound() }
# ── Phase: Hunting -> Celebration -> Lobby ──
EndRound()<suspends>:void =
if (Phase <> shell_hunt_phase.Hunting):
return # already ended (timer AND final shell can both fire)
set Phase = shell_hunt_phase.Celebration
RoundTimer.ResetForAll()
# Winner declared: walk the score map for the top hunter.
var BestScore : int = 0
for (HunterPlayer -> Score : Scores):
if (Score > BestScore):
set BestScore = Score
HuntHud.Show(WinnerMsg(BestScore), ?DisplayTime := 5.0)
# Clean the beach: dispose every runtime-spawned shell.
for (Shell : SpawnedShells):
Shell.Dispose()
set SpawnedShells = array{}
Sleep(5.0) # let the celebration breathe
# Teleport every hunter home (lesson 12) — TeleportTo is failable.
HomeRot := MakeRotation(vector3{X := 0.0, Y := 0.0, Z := 1.0}, 0.0)
for (Hunter : GetPlayspace().GetPlayers()):
if (Character := Hunter.GetFortCharacter[]):
if (Character.TeleportTo[HomePosition, HomeRot]) {}
# Replayable: back to the Lobby, ready for the next press.
set Phase = shell_hunt_phase.Lobby
HuntHud.SetText(WelcomeMsg)
HuntHud.Show()
How the pieces click together
| Region | What's happening |
|---|---|
shell_hunt_kit := module: + using { shell_hunt_kit } |
Lesson 9's kit: localized messages + the shared HUD helper, imported by name. |
shell_hunt_phase := enum{...} |
Lesson 14's three-phase state machine. Every handler checks it first — that's what makes the game un-breakable by button spam. |
OnBegin |
Pure wiring (lesson 8's fan-in): all shell triggers → one handler; button, golden trigger, and timer each get theirs. No game logic lives here. |
StartRound |
Resets state, re-arms triggers (Reset() + Enable()), spawns shells, starts the timer for everyone, pushes 0/{Total} to the HUD. |
SpawnShellProps |
Lesson 10: SpawnProp returns tuple(?creative_prop, spawn_prop_result) — element (0) is optional, so unwrap with ? before keeping the prop for later Dispose(). |
OnShellTouched |
Lesson 4's ?agent unwrap AND the phase gate in one if — both conditions must succeed or the touch is ignored. |
CollectShell / AddScore / CheckWin |
Lesson 7's one-job-each functions. AddScore reads the failable map entry, then set Scores[...] inside an if (map writes are failable too). |
OnGoldenTouched |
Lessons 15-16: 3 points, SpeedBoost.Activate(Hunter), and a 3-second HUD flash. It deliberately does NOT touch ShellsFound — golden is bonus score, not a counted shell. |
OnTimeUp → spawn { EndRound() } |
EndRound carries <suspends> (it Sleeps), but event handlers cannot suspend — so the handler launches it as its own task. |
EndRound |
The guard if (Phase <> ... Hunting): return makes it safe for BOTH the timer and the final shell to call it. Winner from the score map, Dispose() the props, teleport everyone home (failable TeleportTo in an if), back to LOBBY. |
Device wiring checklist
In the UEFN editor, before you launch:
- [ ] Button device on the galleon deck → drag into
StartButton - [ ] HUD Message device (Receivers: All) →
HuntHud - [ ] ~10 Trigger devices, one per shell spot along the beach → add each to the
ShellTriggersarray - [ ] 1 extra Trigger device somewhere sneaky →
GoldenTrigger - [ ] Timer device (Duration: 90s, Start Behavior: manual) →
RoundTimer - [ ] Movement Modulator device →
SpeedBoost - [ ] Shell prop asset (any beach-y prop) →
ShellAsset - [ ] Set
HomePositionto your galleon start line's world coordinates - [ ] A Player Spawner near the galleon so hunters wash ashore in the right place
The ship checklist — playtest end to end
A game isn't shipped until it survives this 8-item pass/fail run. Launch Session with a friend (or two clients) and check every box:
- Start button works — pressing START in LOBBY begins the hunt; pressing it again mid-hunt does nothing.
- Shells spawn — one prop appears above every shell spot at round start.
- Counter updates — every pickup bumps
Shells: n/Totalon the HUD immediately. - Golden shell boosts — the finder gets +3 points, the speed buff, and the HUD flash.
- Timer ends round — when the timer completes, the round ends even with shells left.
- Winner declared — the top score shows in the round-over banner.
- Teleport home — every hunter lands back at the galleon start line.
- Replayable — pressing START again runs a full second round with fresh shells and zeroed scores.
Any box unchecked? The walkthrough table above tells you exactly which function owns that behavior — that's the payoff of lesson 7's one-job-each rule.
Common patterns
Bobbing shells (lesson 11's MoveTo, applied)
Spawned shells sitting flat in the sand are easy to miss. Make them bob forever with a spawned loop per shell:
# Add to shell_hunt_game. Call `spawn { BobShell(Shell) }` right after a
# successful SpawnProp in SpawnShellProps to make every shell bob forever.
BobShell(Shell : creative_prop)<suspends>:void =
StartTransform := Shell.GetTransform()
UpPosition := StartTransform.Translation + vector3{X := 0.0, Y := 0.0, Z := 25.0}
loop:
Shell.MoveTo(UpPosition, StartTransform.Rotation, 1.0)
Shell.MoveTo(StartTransform.Translation, StartTransform.Rotation, 1.0)
Personal counters instead of a shared one
The shared HUD shows the ROUND total. To show each hunter their OWN count, keep a [player]int of personal counts and use the agent-targeted Show overload:
# Swap into CollectShell for a per-hunter count. The kit helper still
# renders the message — only WHO sees it changes (lesson 3's agent overload).
ShowPersonalCount(Hunter : agent, Count : int):void =
HuntHud.SetText(ShellCountMsg(Count, ShellTriggers.Length))
HuntHud.Show(Hunter)
Where this goes next
You just shipped South Shores' capstone — the island's first complete game. Three kinds of doors open from here:
- Polish lessons 18-22 decorate THIS game: the Patchwork drum victory jingle (lesson 18) fires in CELEBRATION, the omega synthesizer's lobby loop (lesson 20) stops when HUNTING starts, and the winner's surfboard lap (lesson 21) rides on the same phase machine.
- Lesson 23 hardens it: handling a hunter who leaves mid-round so the map cleanup and score walk never trip over a gone player.
- Center Village is where Professor AweShucks takes over — and the first thing the hub's integration projects do is
usingyourshell_hunt_kit, exactly the way this capstone did. The module you exported here is South Shores' gift to every zone after it.
Coral's final word: a shipped small game beats a perfect unshipped one. Check all eight boxes, then go make someone play it.