Reference Scene Graph compiles

Dress the Deep: Custom Asset Components (Mesh, VFX, SFX)

Down in the Deeps, presentation is information: a relic that glows through the murk, trails bubbles, and pings like sonar when a diver swims close needs no quest marker. This lesson wires all three asset channels — mesh, VFX and SFX — into one custom `relic_presentation_component` on the lesson-13 relic prefab, driven by lesson 10's `relic_component` carried flag, so every spawned relic in the Athenaeum Depths capstone looks, glows and sounds alive.

Updated Examples verified on the live UEFN compiler

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_component flips 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:

  1. Mesh Component — assign your relic statue mesh.
  2. Light Component (sphere) — the glow; give it a warm color filter and modest intensity.
  3. Particle System Component — assign a bubble-trail Niagara system; leave AutoPlay off (Verse starts it).
  4. Sound Component — assign the sonar-ping sound wave; AutoPlay off here too.
  5. relic_component — lesson 10's component (ID, value, carried state).
  6. 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 with if, so a half-dressed prefab degrades gracefully instead of erroring.
  • sync: inside OnSimulate — the component has two long-running jobs (watch the flag, run the sonar), and sync runs both concurrently in the one lifecycle slot designed for <suspends> work. When the relic entity is removed from the scene, the Scene Graph cancels OnSimulate and both loops die cleanly — no leaked tasks.
  • CarriedChangedEvent.Await() — a Verse event(t) supports Signal and Await, not Subscribe. Native device events (like a trigger's TriggeredEvent) are listenable and give you Subscribe; your own event(logic) is watched with an Await loop. Knowing which is which saves you a compile error every time.
  • Character.GetGlobalTransform().Translation — both the entity and the fort_character speak the modern /Verse.org/SpatialMath dialect through GetGlobalTransform(), so Distance() 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.

Build your own lesson with deeps_custom_asset_component

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 →