Live Scoreboard on the Cannon Wall
In this lesson you'll learn to
- ✓Student can render a live-updating scoreboard (hud_message/billboard refresh loop fed by the score map) that redraws on every score event — UI layering over gameplay.
Student can render a live-updating scoreboard (hud_message/billboard refresh loop fed by the score map) that redraws on every score event — UI layering over gameplay.
📖 Reference & full walkthrough: Live Scoreboard on the Cannon Wall
🔁 Builds on: south-shores: hud-message-device basics; north-jungle: verse-events-custom event bus
🧩 Your capstone piece: The always-on wave/score HUD layer during play
Cinder here. Down on South Shores, Coral taught you to flash a message on the HUD. Up in North Jungle, Barnaby handed you the custom event bus. On the volcano we weld the two together: a scoreboard bolted to the Cannon Wall that redraws itself the instant any score changes — no polling loop, no per-tick refresh, no melted frame budget. This is UI layering over gameplay: the game plays underneath, and a thin display layer listens for one announcement — "a score changed" — and repaints.
What you will build
A cannon_wall_scoreboard Verse device that is the always-on wave/score HUD layer of Volcano Survival — the capstone piece this lesson delivers. It:
- keeps the
[player]intscore map from the previous lesson (Squad Scores with Maps) as the single source of truth, - declares a custom
ScoreChangedEvent : event()— the same event-bus pattern you built in North Jungle's Custom Events lesson — andSignal()s it on every score mutation, - runs one render task that
Await()s that event and repaints a persistenthud_message_devicebanner (SetDisplayTime(0.0), straight from Coral's HUD Message Device lesson on South Shores), - redraws on events, not polling — zero work while nobody scores.
Two trigger plates stand in for "score events" here; in the capstone, elimination and wave-clear events feed the exact same AddScore door.
Walkthrough
The architecture: model → bus → view
Three rules keep a live scoreboard honest:
- One model. The
[player]intmap owns the truth. The HUD never stores scores. - One announcement. Every mutation ends with
ScoreChangedEvent.Signal(). Nothing else signals. - One painter. A single loop
Await()s the bus and repaints the whole board from the whole model.
That last rule is the important one: because the board always renders the current model, a redraw can never show stale or partial state — even if two plates fire in the same breath.
The complete device
Drop this in a new Verse file, build, and wire the three @editable slots in the Details panel (two Trigger devices, one HUD Message device).
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Live scoreboard for the Cannon Wall: a score map + an event bus + one
# persistent HUD banner that redraws ONLY when a score actually changes.
cannon_wall_scoreboard := class(creative_device):
# A plate raiders stomp to score a standard hit.
@editable
ScorePlate : trigger_device = trigger_device{}
# A riskier plate up on the wall, worth more.
@editable
BonusPlate : trigger_device = trigger_device{}
# The HUD banner that IS the scoreboard.
@editable
WallBoard : hud_message_device = hud_message_device{}
# The score model from "Squad Scores with Maps" - one entry per raider.
var Scores : [player]int = map{}
# The event bus from "Custom Events" - one announcement: "a score changed."
ScoreChangedEvent : event() = event(){}
# Localizer helper - hud_message_device text must be of type `message`.
BoardText<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
# DisplayTime 0.0 = persistent. The banner never blinks out on its own.
WallBoard.SetDisplayTime(0.0)
# Gameplay feeds the model...
ScorePlate.TriggeredEvent.Subscribe(OnScorePlate)
BonusPlate.TriggeredEvent.Subscribe(OnBonusPlate)
# ...and the render loop feeds the screen. Paint the empty board once,
# then sleep on the event bus - NO polling, no Sleep-and-check.
RedrawBoard()
loop:
ScoreChangedEvent.Await()
RedrawBoard()
# Standard plate: 10 points.
OnScorePlate(MaybeAgent : ?agent) : void =
if (Scorer := MaybeAgent?):
AddScore(Scorer, 10)
# Bonus plate: 25 points.
OnBonusPlate(MaybeAgent : ?agent) : void =
if (Scorer := MaybeAgent?):
AddScore(Scorer, 25)
# THE ONLY PLACE the model changes - and the only place that signals.
AddScore(Scorer : agent, Points : int) : void =
if (P := player[Scorer]):
var NewTotal : int = Points
if (Existing := Scores[P]):
set NewTotal = Existing + Points
if (set Scores[P] = NewTotal) {}
# Announce the change. The render loop wakes up and redraws.
ScoreChangedEvent.Signal()
# Read the WHOLE model, paint the WHOLE board. Never partial updates.
RedrawBoard() : void =
var CrewTotal : int = 0
var TopScore : int = 0
var Raiders : int = 0
for (P -> S : Scores):
set CrewTotal = CrewTotal + S
set Raiders = Raiders + 1
if (S > TopScore):
set TopScore = S
WallBoard.SetText(BoardText("CANNON WALL | CREW {CrewTotal} | BEST {TopScore} | RAIDERS {Raiders}"))
WallBoard.Show()
Line by line, the parts that matter
| Piece | Why it's there |
|---|---|
var Scores : [player]int = map{} |
The score model — imported straight from Squad Scores with Maps (East Volcano, lesson 3). |
ScoreChangedEvent : event() = event(){} |
Barnaby's event bus from Custom Events (North Jungle). One instance, owned by the device, payload-free: the announcement is the payload. |
WallBoard.SetDisplayTime(0.0) |
Coral's persistence trick from HUD Message Device (South Shores): 0.0 pins the banner on screen until you change it. |
loop: ScoreChangedEvent.Await(); RedrawBoard() |
The render task. Await() suspends — this task costs nothing while nobody scores. This is the "no polling" proof: remove every Sleep-and-check and the board still updates instantly. |
if (P := player[Scorer]) |
The TriggeredEvent hands you an agent; the map is keyed by player. The cast is failable, so it lives in an if. |
if (set Scores[P] = NewTotal) {} |
Writing a map entry is failable in Verse — the empty {} block is the idiomatic "I only care that it succeeded" form. |
RedrawBoard() reads the whole map |
Full repaint from the model. SetText then Show() replaces the previous banner in place. |
Prove it redraws without polling
Run a session with a friend: stand still, touch nothing — the banner sits frozen and your Verse device does zero work (the render task is suspended inside Await()). Stomp a plate — the banner updates the same frame the signal lands. That's the checkpoint: event-driven redraw, not a timer asking "did anything change yet?" thirty times a second.
Common patterns
Pattern 1 — Personal toast layered over the shared board
Two hud_message_devices, two layers: the persistent crew board for everyone, plus a 2-second personal "+10!" flash for the scorer (the agent-targeted Show overload from Coral's lesson).
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Two HUD layers: a persistent shared board + a short personal toast.
layered_scoreboard := class(creative_device):
@editable
ScorePlate : trigger_device = trigger_device{}
# Layer 1: the always-on shared board (DisplayTime 0.0).
@editable
WallBoard : hud_message_device = hud_message_device{}
# Layer 2: a second device for the 2-second personal "+10!" flash.
@editable
PersonalToast : hud_message_device = hud_message_device{}
var Scores : [player]int = map{}
ScoreChangedEvent : event() = event(){}
BoardText<localizes>(S : string) : message = "{S}"
ToastText<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
WallBoard.SetDisplayTime(0.0)
ScorePlate.TriggeredEvent.Subscribe(OnScored)
RedrawBoard()
loop:
ScoreChangedEvent.Await()
RedrawBoard()
OnScored(MaybeAgent : ?agent) : void =
if (Scorer := MaybeAgent?, P := player[Scorer]):
var NewTotal : int = 10
if (Existing := Scores[P]):
set NewTotal = Existing + 10
if (set Scores[P] = NewTotal) {}
# Personal layer: only the scorer sees this, only for 2 seconds.
PersonalToast.Show(Scorer, ToastText("+10! You: {NewTotal}"), ?DisplayTime := 2.0)
# Shared layer: everyone gets the redraw.
ScoreChangedEvent.Signal()
RedrawBoard() : void =
var CrewTotal : int = 0
for (P -> S : Scores):
set CrewTotal = CrewTotal + S
WallBoard.SetText(BoardText("CREW TOTAL: {CrewTotal}"))
WallBoard.Show()
Pattern 2 — Coalescing a burst of signals into one redraw
When a whole squad scores in the same volley, you don't need five repaints. Wake on the first signal, breathe for a tenth of a second, then repaint once. Safe, because the board renders the model, not the event — the latest map state includes every hit that landed during the pause.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Burst coalescing: many signals in one volley, ONE redraw.
coalesced_scoreboard := class(creative_device):
@editable
ScorePlate : trigger_device = trigger_device{}
@editable
WallBoard : hud_message_device = hud_message_device{}
var Scores : [player]int = map{}
ScoreChangedEvent : event() = event(){}
BoardText<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
WallBoard.SetDisplayTime(0.0)
ScorePlate.TriggeredEvent.Subscribe(OnScored)
RedrawBoard()
loop:
# Wake on the FIRST change...
ScoreChangedEvent.Await()
# ...then wait a beat so the rest of the volley lands.
Sleep(0.1)
# One redraw reads the LATEST map state - nothing is lost,
# because the board renders the model, not the event payload.
RedrawBoard()
OnScored(MaybeAgent : ?agent) : void =
if (Scorer := MaybeAgent?, P := player[Scorer]):
var NewTotal : int = 10
if (Existing := Scores[P]):
set NewTotal = Existing + 10
if (set Scores[P] = NewTotal) {}
ScoreChangedEvent.Signal()
RedrawBoard() : void =
var CrewTotal : int = 0
for (P -> S : Scores):
set CrewTotal = CrewTotal + S
WallBoard.SetText(BoardText("CREW TOTAL: {CrewTotal}"))
WallBoard.Show()
Where this goes next
This device is the always-on wave/score HUD layer of Volcano Survival, East Volcano's capstone. The capstone imports it as-is and simply feeds AddScore from real gameplay instead of practice plates:
- Award Points in the Fire Pit (lesson 1) pipes
score_manager_deviceawards into the same map, - Squad Scores with Maps (lesson 3) is the model this board renders,
- the next lesson, End-of-Round Leaderboard from a Map, sorts this very map into a ranked top-N when the round ends — the scoreboard shows the fight live, the leaderboard settles the argument after.
One model, one bus, one painter. Keep those three lines of ownership clean and every HUD layer you add to the volcano stays cheap, current, and correct.
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 lessonSources
/guides/scoreboard-updateBiloxi Studios original lesson
Add this lesson to your free study plan.
🧭 The Keeper’s log
Quest complete? Chart your next heading from the 🐉 East Volcano expedition.
⛵ Take me there