The Athenaeum's relics don't announce themselves with quest markers — this is the Deeps, and down here presentation IS information. A relic that glows through the murk, bubbles as the current slips over it, and pings like sonar when a diver swims close teaches players where to look without a single line of UI. In this lesson you'll drive all three asset channels — mesh, VFX, and SFX — from one custom Verse component, so any entity you dress with it looks, glows, and sounds alive.
What you will build
The relic presentation layer for the Athenaeum Depths capstone: a relic_presentation_component you bolt onto the relic prefab's root entity (the prefab you built in lesson 13, Blueprint of the Vault). It:
- keeps the relic's mesh visible and its glow light on while the relic sits un-carried on the seabed,
- runs a bubble-trail Niagara effect rising off the relic,
- fires a sonar-ping SFX on a heartbeat whenever a diver swims inside
PingRadius, - and the moment lesson 10's
relic_componentflips to carried, shuts the whole show down — mesh off, glow off, bubbles stopped — with one farewell ping.
Every spawned relic in the capstone gets this for free, because it lives on the prefab template, not on any one instance.
Imports from earlier lessons: the relic prefab structure from lesson 13 (scene-graph-prefabs) is the body we're dressing; the carried flag comes from lesson 10's relic_component (Forge a Custom Component); the react-don't-poll wiring is lesson 3 (Events Over Polling); and the Enable-then-Play beats are the same ones east-volcano taught with vfx_spawner_device and the triggered SFX pattern — now translated into their Scene Graph equivalents.
Walkthrough
Step 1 — Know which half of the job belongs to the editor
This is the single most important idea in the lesson. Asset components split their work down the middle:
| Reference | Where it's set | Why |
|---|---|---|
Which mesh asset (the relic statue) the mesh_component renders |
Editor — Details panel on the Mesh Component | Assets are content, not logic |
Which Niagara system (bubble trail) the particle_system_component spawns |
Editor — Details panel | Same |
Which sound wave (sonar ping) the sound_component plays |
Editor — Details panel | Same |
PingRadius, PingInterval on your component |
Editor — because you marked them @editable |
Designer knobs |
Enable() / Disable(), Play() / Stop(), light Intensity |
Verse — at runtime | Behaviour is code |
Verse never picks the asset. It grabs a handle to the component with Entity.GetComponent[...] and drives the switches: on, off, play, stop. If you remember one sentence from the Deeps, make it this one: the editor decides what it is; Verse decides when it happens.
Step 2 — Build the prefab stack in the editor
Open the relic prefab from lesson 13 and, on its root entity, add four stock components plus two Verse ones:
- Mesh Component — assign your relic statue mesh.
- Light Component (sphere) — the glow; give it a warm color filter and modest intensity.
- Particle System Component — assign a bubble-trail Niagara system; leave AutoPlay off (Verse starts it).
- Sound Component — assign the sonar-ping sound wave; AutoPlay off here too.
relic_component— lesson 10's component (ID, value, carried state).relic_presentation_component— the new one below.
Step 3 — Write the component
Create a new Verse file for the component (Add Component > New Verse Component gives you the scaffold). Here is the complete file:
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
# Lesson 10's relic_component, trimmed to the state this lesson reads.
# In your project, keep the full version from "Forge a Custom Component".
relic_component := class<final_super>(component):
# Which relic this is -- matches the RelicLedger IDs from lesson 2.
@editable
var RelicId<public>:int = 0
# true while a diver is carrying this relic to a pedestal.
var Carried<public>:logic = false
# Fires the new carried state. A custom event(t) supports Signal and
# Await (NOT Subscribe) -- listeners run an Await loop. See lesson 3.
CarriedChangedEvent<public>:event(logic) = event(logic){}
SetCarried<public>(NewCarried:logic):void =
set Carried = NewCarried
CarriedChangedEvent.Signal(NewCarried)
# Bolt this onto the relic prefab's ROOT entity (lesson 13). It expects,
# on that same entity:
# * a mesh_component (relic shape -- mesh asset set in editor)
# * a sphere_light_component (the glow)
# * a particle_system_component (bubble-trail Niagara -- asset in editor)
# * a sound_component (sonar-ping wave -- asset in editor)
# * lesson 10's relic_component (the carried flag we react to)
relic_presentation_component := class<final_super>(component):
# Divers inside this radius (in cm) hear the sonar ping.
@editable
var PingRadius<public>:float = 800.0
# Seconds between pings while a diver is in range.
@editable
var PingInterval<public>:float = 1.5
# Runs once when the component starts simulating: set the seabed look.
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
ApplySeabedLook(true)
# OnSimulate is the lifecycle slot built for long-running <suspends>
# work -- the Scene Graph cancels it automatically when the entity
# leaves the scene. sync runs both jobs concurrently, forever.
OnSimulate<override>()<suspends>:void =
sync:
WatchCarried()
SonarLoop()
# Await-loop on the custom event: events over polling (lesson 3).
WatchCarried()<suspends>:void =
if (Relic := Entity.GetComponent[relic_component]):
loop:
IsCarried := Relic.CarriedChangedEvent.Await()
if (IsCarried?):
ApplySeabedLook(false)
# One farewell ping as the relic is scooped up.
if (Sonar := Entity.GetComponent[sound_component]):
Sonar.Play()
else:
# Dropped or re-set on the seabed: light it back up.
ApplySeabedLook(true)
# The sonar heartbeat: ping while the relic is un-carried and a
# diver is close enough to hear it.
SonarLoop()<suspends>:void =
loop:
Sleep(PingInterval)
if:
Relic := Entity.GetComponent[relic_component]
not Relic.Carried?
IsDiverInRange()?
Sonar := Entity.GetComponent[sound_component]
then:
Sonar.Play()
# Flip every asset component in one place: mesh, glow, bubbles.
ApplySeabedLook(Showing:logic):void =
if (Showing?):
if (Mesh := Entity.GetComponent[mesh_component]):
Mesh.Enable()
if (Glow := Entity.GetComponent[sphere_light_component]):
Glow.Enable()
if (Bubbles := Entity.GetComponent[particle_system_component]):
Bubbles.Play()
else:
if (Mesh := Entity.GetComponent[mesh_component]):
Mesh.Disable()
if (Glow := Entity.GetComponent[sphere_light_component]):
Glow.Disable()
if (Bubbles := Entity.GetComponent[particle_system_component]):
Bubbles.Stop()
# true when any active player stands within PingRadius of the relic.
IsDiverInRange():logic =
RelicPos := Entity.GetGlobalTransform().Translation
if (Playspace := Entity.GetPlayspaceForEntity[]):
for:
Player : Playspace.GetPlayers()
Character := Player.GetFortCharacter[]
Character.IsActive[]
do:
if (Distance(Character.GetGlobalTransform().Translation, RelicPos) <= PingRadius):
return true
false
Step 4 — Read the load-bearing lines
Entity.GetComponent[mesh_component]— square brackets because it can fail: maybe nobody added a mesh to this entity. Every asset lookup in this file is guarded withif, so a half-dressed prefab degrades gracefully instead of erroring.sync:insideOnSimulate— the component has two long-running jobs (watch the flag, run the sonar), andsyncruns both concurrently in the one lifecycle slot designed for<suspends>work. When the relic entity is removed from the scene, the Scene Graph cancelsOnSimulateand both loops die cleanly — no leaked tasks.CarriedChangedEvent.Await()— a Verseevent(t)supportsSignalandAwait, notSubscribe. Native device events (like a trigger'sTriggeredEvent) arelistenableand give youSubscribe; your ownevent(logic)is watched with anAwaitloop. Knowing which is which saves you a compile error every time.Character.GetGlobalTransform().Translation— both the entity and thefort_characterspeak the modern/Verse.org/SpatialMathdialect throughGetGlobalTransform(), soDistance()compares them directly. No conversion shims needed.Sonar.Play()— one call, one ping. The sound asset was chosen in the editor; Verse only decides the moment.
Step 5 — Test the beats
Launch a session, swim toward a relic: at 8 meters the pings start. Grab it (your pickup logic calls Relic.SetCarried(true) — in the capstone that's the interactable flow): mesh, glow and bubbles vanish and you hear the farewell ping. Drop it: the seabed look returns. Three asset channels, one component, zero polling of the carried flag.
Common patterns
Pattern 1 — Breathe the glow (Verse-set light intensity)
Light components expose var Intensity (in candela) to Verse, so the glow can breathe instead of sitting statically on. Add this job to the sync block in OnSimulate:
# A slow "breathing" glow: brighter, dimmer, forever.
PulseGlow()<suspends>:void =
if (Glow := Entity.GetComponent[sphere_light_component]):
loop:
set Glow.Intensity = 8.0
Sleep(0.6)
set Glow.Intensity = 2.0
Sleep(0.6)
Note the shape: look the component up once, then loop. This is the same editor-set/Verse-set split again — the light's color filter and radius live in the Details panel; the runtime shimmer is yours.
Pattern 2 — The east-volcano device twin
Not every island is on the Scene Graph yet. The exact same presentation beats exist in the device world — this is the vfx_spawner_device + triggered-SFX pattern you learned on the volcano, and it maps one-to-one:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Device-world twin of the relic presentation: same beats, older tools.
relic_beacon_device := class(creative_device):
@editable
GlowVfx : vfx_spawner_device = vfx_spawner_device{}
@editable
SonarSfx : audio_player_device = audio_player_device{}
@editable
PickupZone : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
GlowVfx.Enable() # glow on: the relic is findable
SonarSfx.Enable()
loop:
MaybeAgent := PickupZone.TriggeredEvent.Await()
GlowVfx.Disable() # picked up: kill the glow...
if (Agent := MaybeAgent?):
SonarSfx.Play(Agent) # ...one-shot ping for the finder
else:
SonarSfx.Play()
Enable/Disable on the spawner ↔ Enable/Disable on particle_system_component; audio_player_device.Play(Agent) ↔ sound_component.Play(). Learn the beats once, speak both dialects.
Where this goes next
This is capstone plumbing, laid deliberately. Lesson 16 (Skeletal Mesh Components & Animation Playback) upgrades the guardian from static mesh to swimming skeleton, and lesson 17 (Lights of the Athenaeum) choreographs the vault-opening cutscene with the same enable/play discipline you used here. In the Athenaeum Depths capstone, every relic the round spawns is an instance of the lesson-13 prefab wearing this exact relic_presentation_component — the dive round's spawner creates N relics, and each one glows, bubbles and pings without another line of presentation code. Dress the prefab once; the Deeps dresses everything else.