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:
- Entities and components from North Jungle (Scene Graph: entities & components and adding components) — an entity is a "thing" in the world, components give it abilities, and one custom
componentsubclass can drive them all. - Eased animation from lesson 13 — the
keyframed_movement_component+ easing-function pattern you used to glide entities (Move It! Glide an entity to a target, plus the lerp mindset) now lives inside the component as the vent's warning rumble.
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
AutoPlayon 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.