Reference Scene Graph compiles

Lava-Vent Prefab — Custom Scene Graph Component with Mesh/VFX/SFX

Everything East Volcano taught you converges here. A custom Scene Graph `component` can drive every other component on its entity — the mesh players see, the particle system that erupts, the sound that booms, and the eased keyframed rumble from the animation lessons — from one piece of Verse. In this lesson you build a complete lava-vent hazard as a single entity tree, wire an eruption loop into the `OnSimulate` lifecycle method, damage every agent caught in the radius, and save the whole thing as a prefab that the Emberpeak Volcano Survival capstone (and every later zone) can import and scatter at will.

Updated Examples verified on the live UEFN compiler

Cinder the dragon has been waiting for this one. Every lesson in East Volcano so far taught you a piece — score plumbing, eased animation, spawners, the storm. Today you forge them into a single reusable asset: a lava vent that rumbles, erupts with fire VFX and a boom of SFX, and scorches anyone standing too close — all bundled into ONE entity you save as a prefab and drop anywhere. This is the Scene Graph payoff: mesh + VFX + SFX + Verse logic travelling together as one thing.

What you will build

The Emberpeak Volcano Survival capstone (the last lesson in this zone) needs erupting lava-vent hazards scattered across the arena. Rather than hand-wiring a mesh, a particle system, a sound, and a script per vent, you will build the vent ONCE as a Scene Graph entity with a custom component, then save it as a prefab. The capstone — and any later zone — imports the prefab and scatters as many vents as it likes. Change the prefab once, every vent on the island updates.

You already have every skill this needs:

The entity tree

Assemble this in the editor first (Scene Graph → Create Entity), exactly the way North Jungle taught you:

LavaVent (root entity)
├── lava_vent_component          ← the Verse logic you write below
├── particle_system_component    ← eruption fire/ember VFX
├── sound_component              ← rumble + boom SFX
└── Plug (child entity)
    ├── mesh_component               ← the rocky plug players see
    └── keyframed_movement_component ← lesson-13 mover for the rumble

Two entities, one job. The root holds the logic, the VFX, and the SFX. The Plug child holds the visible rock and its own mover, so the rumble animation shakes the rock without dragging the particle system around. Because Plug is a child, moving LavaVent moves everything — that is the hierarchy rule from North Jungle doing its quiet work.

Walkthrough

Step 1 — The lifecycle method that holds the loop

A component gets a family of lifecycle methods: OnAddedToScene, OnBeginSimulation, OnSimulate, OnEndSimulation. Only ONE of them is <suspends>OnSimulate. That makes it the home for anything long-running: timers, loops, repeating hazards. OnBeginSimulation must return quickly (it is for setup and subscriptions); OnSimulate is a task the Scene Graph runs for the component's whole life and cancels automatically when the entity leaves the scene.

Our eruption clock is exactly that kind of loop:

OnSimulate → loop → Sleep(interval) → Erupt() → repeat

Step 2 — Telegraph with lesson 13's eased rumble

Fair hazards warn players. Before the vent deals a single point of damage, the rocky plug does a quick eased hop — the same keyframed_movement_delta + ease_in_out_cubic_bezier_easing_function combo you used in the animation lessons, now called from inside the component. FindDescendantComponents(keyframed_movement_component) reaches down into the Plug child to find the mover, no matter how the artist rearranged the tree. That search-don't-hardcode habit is what makes the component reusable.

Step 3 — Fire the senses: VFX and SFX

The eruption itself is two one-liners. particle_system_component and sound_component both expose Play() and Stop(). We fetch them with the failable GetComponent[...] (square brackets — the component might not be there, and a vent with no smoke should not crash the island).

Step 4 — Damage agents in the radius

The vent asks the Scene Graph where it is (Entity.GetGlobalTransform().Translation), asks Fortnite for the playspace (Entity.GetPlayspaceForEntity[] — the component-world equivalent of a device's GetPlayspace()), and measures the distance to each active player. One dragon-sized gotcha: fort_character.GetTransform() speaks the OLD /UnrealEngine.com/Temporary/SpatialMath dialect, while the Scene Graph speaks /Verse.org/SpatialMath. They are different vector3 types and will NOT mix. The bridge is the conversion helper FromVector3, called fully qualified so the two dialects never collide in your imports.

Step 5 — The complete component

using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/SceneGraph }
using { /Verse.org/SceneGraph/KeyframedMovement }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
using { /UnrealEngine.com/Temporary/SpatialMath }

# Bolt this onto the ROOT entity of your lava vent. It expects:
#   * a particle_system_component on the same entity  (eruption VFX)
#   * a sound_component on the same entity            (eruption SFX)
#   * a child entity holding the rocky plug mesh with a
#     keyframed_movement_component                    (the warning rumble)
lava_vent_component := class<final_super>(component):

    # Seconds of calm between eruptions.
    @editable
    var EruptionInterval<public>:float = 6.0

    # How many damage pulses one eruption deals.
    @editable
    var DamagePulses<public>:int = 4

    # Seconds between damage pulses while erupting.
    @editable
    var PulseGap<public>:float = 0.5

    # Agents inside this radius (in cm) take damage each pulse.
    @editable
    var DamageRadius<public>:float = 400.0

    # Damage dealt per pulse.
    @editable
    var DamagePerPulse<public>:float = 15.0

    # How far the plug hops during the warning rumble, in cm.
    @editable
    var RumbleHeight<public>:float = 40.0

    # The eruption clock. OnSimulate is the component's <suspends>
    # lifecycle method -- the one slot designed to hold a
    # long-running loop like this. It is cancelled automatically
    # when the entity leaves the scene.
    OnSimulate<override>()<suspends>:void =
        loop:
            Sleep(EruptionInterval)
            Erupt()

    # One full eruption: rumble warning, then VFX + SFX + damage pulses.
    Erupt()<suspends>:void =
        RumblePlug()      # telegraph first -- fair hazards warn players
        Sleep(0.8)        # the rumble plays; players get a beat to run

        if (VFX := Entity.GetComponent[particle_system_component]):
            VFX.Play()
        if (SFX := Entity.GetComponent[sound_component]):
            SFX.Play()

        var Pulse:int = 0
        loop:
            DamageAgentsInRadius()
            Sleep(PulseGap)
            set Pulse += 1
            if (Pulse >= DamagePulses):
                break

        if (VFX := Entity.GetComponent[particle_system_component]):
            VFX.Stop()

    # Lesson 13's eased animation, now INSIDE the component: hop the
    # plug up and settle it back down with an ease-in-out curve.
    RumblePlug():void =
        for (Mover : Entity.FindDescendantComponents(keyframed_movement_component)):
            RiseStep := keyframed_movement_delta:
                Duration := 0.4
                Easing := ease_in_out_cubic_bezier_easing_function{}
                Transform := (/Verse.org/SpatialMath:)transform:
                    Translation := (/Verse.org/SpatialMath:)vector3:
                        Forward := 0.0
                        Left := 0.0
                        Up := RumbleHeight
            SettleStep := keyframed_movement_delta:
                Duration := 0.4
                Easing := ease_in_out_cubic_bezier_easing_function{}
                Transform := (/Verse.org/SpatialMath:)transform:
                    Translation := (/Verse.org/SpatialMath:)vector3:
                        Forward := 0.0
                        Left := 0.0
                        Up := -RumbleHeight
            Mover.SetKeyframes(array{RiseStep, SettleStep}, oneshot_keyframed_movement_playback_mode{})
            Mover.Play()

    # Hurt every active player standing within DamageRadius of the vent.
    DamageAgentsInRadius():void =
        VentPos := Entity.GetGlobalTransform().Translation
        if (Playspace := Entity.GetPlayspaceForEntity[]):
            for:
                Player : Playspace.GetPlayers()
                Character := Player.GetFortCharacter[]
                Character.IsActive[]
            do:
                # fort_character positions use the OLD SpatialMath dialect;
                # convert before comparing with the Scene Graph's vector3.
                CharPos := (/UnrealEngine.com/Temporary/SpatialMath:)FromVector3(Character.GetTransform().Translation)
                if ((/Verse.org/SpatialMath:)Distance(CharPos, VentPos) <= DamageRadius):
                    Character.Damage(DamagePerPulse)

Reading the important lines

Line Why it matters
OnSimulate<override>()<suspends>:void The ONLY <suspends> lifecycle method — the eruption loop lives here, and the Scene Graph cancels it for you when the entity is removed.
Entity.FindDescendantComponents(keyframed_movement_component) Searches the whole subtree, so the mover on the Plug child is found without hard-coding the layout.
Entity.GetComponent[particle_system_component] Square brackets = failable. No VFX component? The if simply skips — no crash.
Entity.GetPlayspaceForEntity[] Components have no GetPlayspace() like devices do; this is how an entity reaches the playspace (failable — the entity might not be in a scene yet).
(/UnrealEngine.com/Temporary/SpatialMath:)FromVector3(...) Bridges the character's old-dialect position into the Scene Graph's vector3 so Distance type-checks. Called fully qualified so we never using both dialects at once.
Character.Damage(DamagePerPulse) fort_character implements damageable — one call, real damage.

Step 6 — Save it as a prefab

In the editor: select the LavaVent root entity in the Outliner, right-click → Create Prefab, name it LavaVent, and save it under your project's Content folder. The mesh, VFX, SFX, mover, child hierarchy, and your lava_vent_component (with all its @editable knobs) are now ONE draggable asset. Drop three into a test level, set different EruptionInterval values on each, hit Launch Session — three independent vents, one source of truth.

Wiring checklist

  • Root entity: lava_vent_component, particle_system_component (pick a fire/ember Niagara asset), sound_component (pick your boom).
  • Child Plug: mesh_component (rocky plug), keyframed_movement_component.
  • Turn OFF AutoPlay on the particle and sound components — the Verse logic decides when they fire.
  • Sanity: the plug mesh should be small enough that the eased hop reads as a rumble, not a rocket.

Common patterns

Pattern 1 — Distance falloff (the lerp mindset returns)

Flat damage feels arcadey. Scale it by proximity — a player at the rim takes a graze, a player standing ON the vent takes the full roast. This is lesson 13's interpolation idea applied to damage:

# Inside DamageAgentsInRadius, replace the flat damage call:
    Dist := Distance(CharPos, VentPos)
    if (Dist <= DamageRadius):
        # 1.0 at the center, fading to 0.0 at the rim.
        Falloff := 1.0 - (Dist / DamageRadius)
        Character.Damage(DamagePerPulse * Falloff)

Pattern 2 — Pressure vent (event-driven instead of timed)

Swap the clock for a trap: erupt when something steps onto the plug. mesh_component fires EntityEnteredEvent when another entity overlaps it (make sure Queryable is enabled on the mesh). Subscriptions belong in OnBeginSimulation — quick setup, no loop:

using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }

# A vent that erupts when ANY entity overlaps its plug mesh.
pressure_vent_component := class<final_super>(component):

    OnBeginSimulation<override>():void =
        (super:)OnBeginSimulation()
        for (Mesh : Entity.FindDescendantComponents(mesh_component)):
            Mesh.EntityEnteredEvent.Subscribe(OnStepped)

    OnStepped(Intruder:entity):void =
        if (VFX := Entity.GetComponent[particle_system_component]):
            VFX.Play()
        if (SFX := Entity.GetComponent[sound_component]):
            SFX.Play()

Both vents can coexist as separate prefabs — LavaVent and PressureVent — built from the same parts.

Where this goes next

This prefab IS East Volcano's export. The zone capstone, Emberpeak Volcano Survival, scatters erupting lava-vent hazards through the arena — that is your LavaVent prefab dropped a dozen times with staggered EruptionInterval values, layered under the shrinking storm from the storm-controller lesson and the creature waves from the spawner lessons. And because a prefab travels, later zones import it too: West Coves will debug a misbehaving vent's async timing, and The Deeps will treat this component as the warm-up for full scene-graph mastery. One vent, forged once, erupting everywhere. Cinder approves.

Build your own lesson with verse_scene_graph_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 →