Builtin Library
README.md
entity_signals
Per-entity destroy signals, surfaced on the entity proxy as
entity(id).onDestroying and entity(id).onDestroyed, plus the dispatch
the despawn pipeline calls to fire them.
local boss = entity.find("Boss")
boss.onDestroying:Connect(function()
-- runs BEFORE teardown — final state is still readable
saveBossStats(boss.component.get("Combatant").public)
end)
boss.onDestroyed:Connect(function()
log.info("boss fully cleaned up")
end)
entity.despawn(boss.id) -- onDestroying fires, then teardown, then onDestroyed
Order
onDestroying and onDestroyed are Signals (see signal.module),
created on first access. The despawn pipeline drives the order. For a
cascade rooted at one entity.despawn(root) call:
onDestroyingfires across the whole subtree, root first — before any component teardown, so handlers read final state.- components tear down (
onDestroy(self)per component), leaves first. onDestroyedfires per entity as it is removed, leaves first.
onDestroying handlers must not yield, and the entity is frozen against
reparenting / re-mutation during the destroying window.
An entity nobody observes costs nothing and fires nothing.
Entity-keyed tables
A module that keeps per-entity state for something to read every frame (a
registry a render feature walks, a set of marked entities) declares its table
with entityKeyed. When an entity is despawned, by entity.despawn, a
parent's cascade, a scene rebuild or reload, or a mode flip, its id leaves
every such table, so the reader only ever meets entities in the world and its
cost follows them.
local EntitySignals = require("@builtin::modules.entity_signals")
local marked = EntitySignals.entityKeyed({})
function M.mark(id: string)
marked[id] = true
end
The table is one of the engine's own records: a module-state restore leaves it as it stands, so an id that left it stays out.