Capstone: Barnaby's Jungle Mod-Rally

In this lesson you'll learn to

  • ✓Assemble every lesson into one playable rally mode: state machine, checkpoints, mod boxes, boosts, HUD, and persistence — launchable as a real island.

Assemble every lesson into one playable rally mode: state machine, checkpoints, mod boxes, boosts, HUD, and persistence — launchable as a real island.

📖 Reference & full walkthrough: Capstone: Barnaby's Jungle Mod-Rally

🔁 Builds on: south-shores ShellHunt game-phase enum + SpawnProp helper; center-village custom-round-logic round loop; town matchmaking-portal launch

🧩 Your capstone piece: The full game — this IS the capstone assembly lesson


Welcome to the summit, racer. Barnaby has watched you climb every rung of the Codewood Grove ladder — values, loops, arrays, classes, events, devices, HUD widgets — and now it is time to bolt all of it onto one frame and kick the starter. This is the assembly lesson: everything you built in the last 36 lessons becomes one playable rally island.

What you will build

Barnaby's Jungle Mod-Rally: a 2-lap dirtbike race through Codewood Grove. Racers gather on the starting grid, a drum-beat countdown fires, and the race_phase state machine flips to Racing. Riders follow the ordered checkpoint chain — Clifftop Lookout, Rope-Bridge Canyon, Zipline Run, Waterfall Pool — and hit the Sunrise Summit mod station, where a vehicle mod box awards a rolled mod. Banana boosts spawn on the track; each racer's laps and boosts show on a per-player canvas HUD. First to finish the final lap wins, and the portal back to AweShucks Town opens.

Architecture at a glance

Every system below is a lesson you have already finished — this article only wires them:

System Built in lesson
race_phase enum state machine (Lobby → Countdown → Racing → Finished) sug-defining-and-using-enums-in-verse (pattern imported from south-shores ShellHunt)
Lobby ready-gate (player_counter_device holds the start) player-counter-device
Ordered checkpoint chain (trigger_device sensors + array indexing) trigger-device, verse-arrays-to-keep-track-of-many-things-at-once
CheckpointPassedEvent(racer, index) event bus verse-events-custom
3-2-1-GO drum countdown drum-player-device, verse-loops-to-repeat-actions-on-your-island
Sunrise Summit mod station (vehicle_mod_box_spawner_device) vehicle-mod-box-spawner-device
Banana boosts (item_spawner_device pickups + item_granter_device rewards) item-spawner-device, item-granter-device
Per-racer lap/boost HUD (GetPlayerUI + canvas + text_block) creating-a-simple-ui-with-hud-widgets-in-verse, canvas-layout
LapsOf / BoostsOf [player]int maps verse-maps-to-look-things-up-by-a-key
Checkpoint fanfare speaker speaker-device
Launch/return portal center-village matchmaking-portal-device

Walkthrough

Create a new Verse file called rally_game.verse, drop the device into your island, and wire every @editable (the checklist below tells you what goes where). Read the flow first, then the full listing.

Step 1 — the phase machine. race_phase is the ShellHunt enum pattern from south-shores, renamed for the jungle: every event handler checks Phase before acting, so stray trigger hits during the lobby or after the finish do nothing. One enum, zero soft-locks.

Step 2 — checkpoint sensors. Subscribe only delivers the ?agent payload — it cannot tell you which trigger fired. So we mint one tiny checkpoint_sensor object per gate, each carrying its route Index, and subscribe its method. This is the classes lesson earning its keep.

Step 3 — the lobby gate. The player_counter_device holds the race until RacersNeeded riders stand on the grid, then fires CountSucceedsEvent exactly like the vault-door pattern you built in the player-counter lesson. CompareToTarget() at OnBegin catches racers who beat the script there.

Step 4 — countdown and GO. RunRace is the center-village custom-round-logic loop reshaped: arm, drum countdown (a plain loop with a counter — your loops lesson), flip to Racing, stamp RaceStartTime with GetSimulationElapsedTime(), zero every racer's maps, and spawn the first mod box.

Step 5 — order enforcement. HandleCheckpoint only advances a racer whose NextCheckpointOf map entry matches the gate index — the if/else validation lesson. Passing the last gate calls CompleteLap; finishing lap LapCount wins.

Step 6 — the event bus. Checkpoints Signal the CheckpointPassedEvent; the announcer task in OnBegin Awaits it forever and handles HUD refresh + speaker fanfare. Checkpoints never touch the HUD; the announcer never touches triggers. That decoupling is the custom-events lesson, and it is what keeps a 37-lesson game maintainable.

Here is the complete, compilable capstone device:

using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Fortnite.com/Vehicles }
using { /Fortnite.com/UI }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/Diagnostics }

# ── Imported from south-shores: the ShellHunt game-phase enum pattern. ──
# Same shape Coral used on the beach: every handler checks the phase first.
race_phase := enum:
    Lobby
    Countdown
    Racing
    Finished

# One sensor object per checkpoint gate. Subscribe only hands us the ?agent
# payload, so the sensor carries WHICH gate fired and forwards both to the game.
checkpoint_sensor := class:
    Game : rally_game_device
    Index : int

    OnTriggered(MaybeAgent : ?agent) : void =
        Game.HandleCheckpoint(MaybeAgent, Index)

# ── The capstone device: rally_game.verse ──
rally_game_device := class(creative_device):

    # ── Track devices — wire EVERY one of these in the Details panel. ──
    @editable
    LobbyGate : player_counter_device = player_counter_device{}

    @editable
    Checkpoints : []trigger_device = array{}

    @editable
    BikeSpawners : []vehicle_spawner_dirtbike_device = array{}

    @editable
    ModStation : vehicle_mod_box_spawner_device = vehicle_mod_box_spawner_device{}

    @editable
    BoostSpawner : item_spawner_device = item_spawner_device{}

    @editable
    BoostGranter : item_granter_device = item_granter_device{}

    @editable
    CountdownDrums : drum_player_device = drum_player_device{}

    @editable
    FanfareSpeaker : speaker_device = speaker_device{}

    @editable
    ReturnPortal : matchmaking_portal_device = matchmaking_portal_device{}

    @editable
    LapCount : int = 2

    @editable
    RacersNeeded : int = 2

    # ── Race state ──
    var Phase : race_phase = race_phase.Lobby
    var LapsOf : [player]int = map{}
    var NextCheckpointOf : [player]int = map{}
    var BoostsOf : [player]int = map{}
    var HudTexts : [player]text_block = map{}
    var RaceStartTime : float = 0.0

    # ── The CheckpointPassedEvent bus from the custom-events lesson. ──
    # Checkpoints Signal it; the announcer task Awaits it. Neither knows the other.
    CheckpointPassedEvent : event(tuple(player, int)) = event(tuple(player, int)){}

    # Text widgets take messages, never raw strings.
    RallyText<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends> : void =
        # The portal home stays shut until someone wins.
        ReturnPortal.Disable()
        ModStation.Enable()

        # ── Lobby gate: the race arms only when the grid is full. ──
        LobbyGate.SetTargetCount(RacersNeeded)
        LobbyGate.CountSucceedsEvent.Subscribe(OnGridFull)

        # One sensor per checkpoint gate, each remembering its route index.
        for (Index -> Gate : Checkpoints):
            Sensor := checkpoint_sensor{Game := Self, Index := Index}
            Gate.TriggeredEvent.Subscribe(Sensor.OnTriggered)

        # Banana boosts scattered on the track.
        BoostSpawner.ItemPickedUpEvent.Subscribe(OnBoostPickedUp)

        # Evaluate immediately in case racers are already on the grid.
        LobbyGate.CompareToTarget()

        # ── Announcer task: reacts to every checkpoint pass, forever. ──
        loop:
            Passed := CheckpointPassedEvent.Await()
            UpdateHud(Passed(0))
            FanfareSpeaker.Enable()
            Sleep(0.4)
            FanfareSpeaker.Disable()

    # ── Phase: Lobby → Countdown. Fired by the player counter. ──
    OnGridFull(Unused : tuple()) : void =
        if (Phase = race_phase.Lobby):
            spawn { RunRace() }

    # ── Imported from center-village: the custom-round-logic round loop ──
    # (arm → countdown → play → finish), reshaped into rally phases.
    RunRace()<suspends> : void =
        set Phase = race_phase.Countdown

        # Bikes appear as the drums start.
        for (Spawner : BikeSpawners):
            Spawner.Enable()

        # 3-2-1-GO on the drum player.
        CountdownDrums.Enable()
        var Count : int = 3
        loop:
            if (Count <= 0):
                break
            Print("Race starts in {Count}...")
            Sleep(1.0)
            set Count -= 1
        CountdownDrums.Disable()

        # GO!
        set Phase = race_phase.Racing
        set RaceStartTime = GetSimulationElapsedTime()
        for (Racer : GetPlayspace().GetPlayers()):
            if (set LapsOf[Racer] = 0, set NextCheckpointOf[Racer] = 0, set BoostsOf[Racer] = 0) {}
            ShowHud(Racer)
        ModStation.SpawnBox()
        Print("GO!")

    # ── Called by every checkpoint_sensor. Gates only count IN ORDER. ──
    HandleCheckpoint(MaybeAgent : ?agent, Index : int) : void =
        if (Phase = race_phase.Racing, Agent := MaybeAgent?, Racer := player[Agent]):
            if (Expected := NextCheckpointOf[Racer], Expected = Index):
                NextIndex := Index + 1
                if (NextIndex >= Checkpoints.Length):
                    CompleteLap(Racer)
                else:
                    if (set NextCheckpointOf[Racer] = NextIndex) {}
                CheckpointPassedEvent.Signal(Racer, Index)

    CompleteLap(Racer : player) : void =
        var NewLaps : int = 1
        if (Prev := LapsOf[Racer]):
            set NewLaps = Prev + 1
        if (set LapsOf[Racer] = NewLaps, set NextCheckpointOf[Racer] = 0) {}
        if (NewLaps >= LapCount):
            OnRacerWins(Racer)

    # ── Finish: fanfare, portal home opens (the AweShucks Town launch loop). ──
    OnRacerWins(Racer : player) : void =
        if (Phase = race_phase.Racing):
            set Phase = race_phase.Finished
            RaceTime := GetSimulationElapsedTime() - RaceStartTime
            var WholeSeconds : int = 0
            if (Secs := Floor[RaceTime]):
                set WholeSeconds = Secs
            Print("Winner! Race time: {WholeSeconds}s")
            if (Hud := HudTexts[Racer]):
                Hud.SetText(RallyText("WINNER — {WholeSeconds}s! The portal home is open."))
            # Imported from center-village: the matchmaking-portal launch point.
            ReturnPortal.Enable()

    # ── Banana boost picked up on the track. ──
    OnBoostPickedUp(Agent : agent) : void =
        if (Racer := player[Agent]):
            var NewBoosts : int = 1
            if (Prev := BoostsOf[Racer]):
                set NewBoosts = Prev + 1
            if (set BoostsOf[Racer] = NewBoosts) {}
            BoostGranter.GrantItem(Agent)
            UpdateHud(Racer)

    # ── Per-racer canvas HUD: lap counter + boost meter. ──
    ShowHud(Racer : player) : void =
        if (UI := GetPlayerUI[Racer]):
            Hud := text_block:
                DefaultText := RallyText("Lap 1/{LapCount}  |  Boosts 0")
            HudCanvas := canvas:
                Slots := array:
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.08}, Maximum := vector2{X := 0.5, Y := 0.08}}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Hud
            UI.AddWidget(HudCanvas)
            if (set HudTexts[Racer] = Hud) {}

    UpdateHud(Racer : player) : void =
        var Laps : int = 0
        if (L := LapsOf[Racer]):
            set Laps = L
        var Boosts : int = 0
        if (B := BoostsOf[Racer]):
            set Boosts = B
        ShownLap := Min(Laps + 1, LapCount)
        if (Hud := HudTexts[Racer]):
            Hud.SetText(RallyText("Lap {ShownLap}/{LapCount}  |  Boosts {Boosts}"))

Reading the listing

  • checkpoint_sensor{Game := Self, Index := Index} — fields with no defaults are set at instantiation; each sensor is a permanent little name-tag for its gate.
  • if (set LapsOf[Racer] = 0, ...) {} — map writes are failable; the if (...) {} idiom acknowledges that without drama.
  • Floor[RaceTime] — Floor is <decides>, so it is called in a failure context; we fall back to 0 rather than crash on a weird float.
  • The announcer loop lives at the bottom of OnBegin, which is <suspends> — the one place you can park an infinite Await loop for free.

Device wiring checklist

@editable Device to place Where on the island
LobbyGate 1x Player Counter Starting-grid clearing at Palm Grove
Checkpoints 5x Trigger (ordered!) Clifftop Lookout → Rope-Bridge Canyon → Zipline Run → Waterfall Pool → finish arch (index order = race order)
BikeSpawners 4-8x Vehicle Spawner (Dirtbike) Behind the start line
ModStation 1x Vehicle Mod Box Spawner Sunrise Summit platform
BoostSpawner 1x Item Spawner (banana boost item) Banana-plantation straight
BoostGranter 1x Item Granter (reward item list) Anywhere (logic-only)
CountdownDrums 1x Drum Player (Patchwork) Start gantry
FanfareSpeaker 1x Speaker (Patchwork) Start/finish arch
ReturnPortal 1x Matchmaking Portal (island code = AweShucks Town) Podium at Verdant Hollow

Compile with Verse → Build Verse Code, wire, then Launch Session. The final checkpoint of the whole north-jungle path is exactly this: a compile-passing rally_game.verse with every @editable wired.

What this capstone imports

  • south-shores — ShellHunt game-phase enum: race_phase is the same LOBBY → PLAY → CELEBRATE machine Coral gated every shell handler behind, with a Countdown phase spliced in for the drums.
  • south-shores — SpawnProp helper: the rope-bridge swinging gate (Common patterns below) runtime-spawns its prop exactly the way ShellHunt scattered shells.
  • center-village — custom-round-logic round loop: RunRace is Professor AweShucks' arm → play → win → reset skeleton wearing racing leathers; the session-scoped weak_map best-time board below is the same lesson's storage shape.
  • center-village — matchmaking-portal launch: the rally ships as a real island launched from an AweShucks Town portal booth, and ReturnPortal closes the loop home when the flag drops.

Common patterns

Pattern 1 — the rope-bridge swinging gate (SpawnProp import, live)

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

# The rope-bridge hazard, built with the SAME SpawnProp helper Coral used
# to scatter shells on South Shores — here it spawns the swinging gate.
swinging_gate_hazard := class(creative_device):

    @editable
    GateAsset : creative_prop_asset = DefaultCreativePropAsset

    OnBegin<override>()<suspends> : void =
        SpawnResult := SpawnProp(GateAsset, GetTransform())
        if (Gate := SpawnResult(0)?):
            Base := Gate.GetTransform()
            # Swing across the bridge and back, forever.
            loop:
                Gate.MoveTo(Base.Translation + vector3{X := 0.0, Y := 300.0, Z := 0.0}, Base.Rotation, 1.5)
                Gate.MoveTo(Base.Translation, Base.Rotation, 1.5)

MoveTo is <suspends>, so each swing completes before the return swing starts — a two-line animation loop, no timeline asset required.

Pattern 2 — session best-time board for east-volcano

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

# Session-scoped best-time board: survives round resets within the match,
# using the same weak_map(session, ...) shape as the center-village round loop.
var RallyBestTimes : weak_map(session, [player]int) = map{}

best_time_board := class(creative_device):

    RecordBestTime(Racer : player, WholeSeconds : int) : void =
        Session := GetSession()
        var Board : [player]int = if (Existing := RallyBestTimes[Session]) then Existing else map{}
        var IsBest : logic = true
        if (Prev := Board[Racer], Prev <= WholeSeconds):
            set IsBest = false
        if (IsBest?):
            if (set Board[Racer] = WholeSeconds, set RallyBestTimes[Session] = Board):
                Print("New rally record for this racer: {WholeSeconds}s")

Call RecordBestTime from OnRacerWins and the board survives round resets — east-volcano's unlock checks read exactly this kind of per-player record.

Where this goes next

You just shipped the north-jungle capstone — the second full game on the archipelago. Three things travel onward from here:

  • CodewoodKit (your creating-custom-modules lesson): FormatTime, Distance, and PickRandom are imported by east-volcano, west-coves, and the-deeps.
  • The checkpoint prefab (scene-graph-add-component): east-volcano's wave arenas and the-deeps' dive route reuse the same entity + component gate.
  • Best-time maps: east-volcano's Emberpeak Trials reads your rally records for its unlock gates.

Publish the island, hand the code to the AweShucks Town portal booth, and go watch strangers race your jungle. Barnaby will be at the summit, timing laps. Cinder the dragon is already warming up the crater next door — east-volcano awaits.

🔒

Keep going — free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/north-jungle-mod-rally
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the 🐻 North Jungle expedition.

⛵ Take me there