Log inGet started

effects

Updated 5 September 2026

The effects namespace — 31 functions.

globals/effects/backends

effects.backends() -> { string }

The backend kinds an effect can be built out of, in name order. The runtime ships emitter, geometry, material, decal and feature.

Returns { string } — Array of kind names.

print(table.concat(effects.backends(), ", "))

globals/effects/describe

effects.describe(identity: string) -> { [string]: any }

What an effect declares about itself: its family, a one-line summary, every parameter with its type, default and documented range, and the cost one unpooled play of it was measured to draw. The one call to make against an unfamiliar effect before playing it.

Parameters

  • identity string — The effect's canonical identity, or a short name.

Returns { [string]: any }{ identity, family, summary, cost, params }.

local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)

globals/effects/drain

effects.drain() -> { [string]: number }

Free every backend the pool is holding idle. The pool keeps what it has leased for as long as the engine runs — that is what makes repeated firing cost nothing after the first — and this is the one call that gives it back. A backend a live play still holds is left to that play's own end.

Returns { [string]: number }{ freed, kept }.

print(effects.drain().freed)

globals/effects/families

effects.families() -> { string }

Every family the effects in this world declare, sorted — the values list { family = … } filters on. An effect declaring no family is not one of them.

Returns { string } — Array of family names.

for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end

globals/effects/list

effects.list(opts: table?) -> { string }

The canonical identity of every effect this world can play, sorted. These are the exact strings play takes. Pass { family = "combat" } to get only the effects of one family — the catalogue filtered the way an effect declares itself.

Parameters

  • opts table (optional){ family? = string }. A family is matched without regard to case.

Returns { string } — Array of identities.

for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end

globals/effects/observe

effects.observe() -> { [string]: any }

What the runtime is holding and driving right now — every live play with the reason it is silent when it is, plus what the pool has leased out and what it is keeping idle, in instances and in GPU bytes. This is how a caller and a test tell a working effect from a silent one, and how they tell a pool warming to a wider burst from something leaking: the pool is sized by the most effects it has had to cover at once, which peakLive and peakLeased report beside the current totals.

Returns { [string]: any } — The observation.

local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers

globals/effects/play

effects.play(identity: string, opts: table?) -> any

Play an effect once at a world position. The effect allocates what it needs from the shared pool, draws itself, and gives everything back when it ends — with no update loop on the caller's side.

Parameters

  • identity string — The effect's canonical identity, or a short name that reaches exactly one effect.
  • opts table (optional){ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z }, params? = { … }, duration? = number, held? = boolean }. Anything params omits takes the effect's declared default, and an effect that declares a duration parameter reads its length from there rather than from duration here.

Returns any — The play handle — stop, cancel, retarget, setParam, isPlaying, isFinished, seek, stats, whySilent.

local h = effects.play("@builtin::systems.effects.combat.explosion", {
position = { 0, 2, 0 }, params = { scale = 4, coreColor = { 1, 0.4, 0.1 } },
})

globals/effects/playOn

effects.playOn(identity: string, target: any?, opts: table?) -> any

Play an effect on an entity: it starts where the entity stands and ends if the entity leaves the world. Move it with the entity by calling handle:retarget(theEntity) as it goes.

Parameters

  • identity string — The effect's canonical identity, or a short name.
  • target any (optional) — An entity proxy or entity id.
  • opts table (optional) — The same options play takes; position is read from the entity.

Returns any — The play handle.

local h = effects.playOn("explosion", drum, { params = { scale = 3 } })

globals/effects/registerBackend

effects.registerBackend(kind: string, backend: table)

Register a new way of drawing under a kind name, so an effect family that needs one the runtime does not ship adds it rather than widening the runtime. Every effect reaches it through ctx.lease(kind, spec).

Parameters

  • kind string — The kind name a spec asks for.
  • backend table — The backend — key, acquire, seat, start, stop, quiet, place, bytes, active, silence and free.
effects.registerBackend("ribbonTrail", myBackend)

globals/effects/silenceReasons

effects.silenceReasons() -> { { reason: string, means: string } }

The closed set of reasons a play can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every reason an observation reports is one of these.

Returns { { reason: string, means: string } } — Array of { reason, means }.

for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end

modules/effects/README

require("@builtin/modules/api/engine/effects") -- effects (also available as global 'effects')

Fire a finished visual effect from gameplay code in one line. play puts an effect at a position and playOn sticks it to an entity; both return a handle that stops it early, moves it, or re-tunes a parameter while it runs. The effect owns its own lifetime — nothing here needs the caller to tick it — and repeated firing re-uses what the last one left rather than allocating again.

Usage: local effects = require("@builtin/modules/api/engine/effects") Also available as global: effects

modules/effects/backends

backends(): { string }

The backend kinds an effect can be built out of, in name order. The runtime ships emitter, geometry, material, decal and feature.

print(table.concat(effects.backends(), ", "))

modules/effects/describe

describe(identity: string): { [string]: any }

What an effect declares about itself: its family, a one-line summary, every parameter with its type, default and documented range, and the cost one unpooled play of it was measured to draw. The one call to make against an unfamiliar effect before playing it.

Parameters

  • identity string — The effect's canonical identity, or a short name.
local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)

modules/effects/drain

drain(): { [string]: number }

Free every backend the pool is holding idle. The pool keeps what it has leased for as long as the engine runs — that is what makes repeated firing cost nothing after the first — and this is the one call that gives it back. A backend a live play still holds is left to that play's own end.

print(effects.drain().freed)

modules/effects/families

families(): { string }

Every family the effects in this world declare, sorted — the values list { family = … } filters on. An effect declaring no family is not one of them.

for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end

modules/effects/list

list(opts: table?): { string }

The canonical identity of every effect this world can play, sorted. These are the exact strings play takes. Pass { family = "combat" } to get only the effects of one family — the catalogue filtered the way an effect declares itself.

Parameters

  • opts table? (optional){ family? = string }. A family is matched without regard to case.
for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end

modules/effects/observe

observe(): { [string]: any }

What the runtime is holding and driving right now — every live play with the reason it is silent when it is, plus what the pool has leased out and what it is keeping idle, in instances and in GPU bytes. This is how a caller and a test tell a working effect from a silent one, and how they tell a pool warming to a wider burst from something leaking: the pool is sized by the most effects it has had to cover at once, which peakLive and peakLeased report beside the current totals.

local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers

modules/effects/play

play(identity: string, opts: table?): any

Play an effect once at a world position. The effect allocates what it needs from the shared pool, draws itself, and gives everything back when it ends — with no update loop on the caller's side.

Parameters

  • identity string — The effect's canonical identity, or a short name that reaches exactly one effect.
  • opts table? (optional){ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z }, params? = { … }, duration? = number, held? = boolean }. Anything params omits takes the effect's declared default, and an effect that declares a duration parameter reads its length from there rather than from duration here.
local h = effects.play("@builtin::systems.effects.combat.explosion", {
position = { 0, 2, 0 }, params = { scale = 4, coreColor = { 1, 0.4, 0.1 } },
})

modules/effects/playOn

playOn(identity: string, target: any, opts: table?): any

Play an effect on an entity: it starts where the entity stands and ends if the entity leaves the world. Move it with the entity by calling handle:retarget(theEntity) as it goes.

Parameters

  • identity string — The effect's canonical identity, or a short name.
  • target any (optional) — An entity proxy or entity id.
  • opts table? (optional) — The same options play takes; position is read from the entity.
local h = effects.playOn("explosion", drum, { params = { scale = 3 } })

modules/effects/registerBackend

registerBackend(kind: string, backend: table)

Register a new way of drawing under a kind name, so an effect family that needs one the runtime does not ship adds it rather than widening the runtime. Every effect reaches it through ctx.lease(kind, spec).

Parameters

  • kind string — The kind name a spec asks for.
  • backend table — The backend — key, acquire, seat, start, stop, quiet, place, bytes, active, silence and free.
effects.registerBackend("ribbonTrail", myBackend)

modules/effects/silenceReasons

silenceReasons(): { { reason: string, means: string } }

The closed set of reasons a play can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every reason an observation reports is one of these.

for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end

typed/builtin//modules/api/engine/effects/effects/backends

effects.backends() -> { string }

The backend kinds an effect can be built out of, in name order. The runtime ships emitter, geometry, material, decal and feature.

Returns { string } — Array of kind names.

print(table.concat(effects.backends(), ", "))

typed/builtin//modules/api/engine/effects/effects/describe

effects.describe(identity: string) -> { [string]: any }

What an effect declares about itself: its family, a one-line summary, every parameter with its type, default and documented range, and the cost one unpooled play of it was measured to draw. The one call to make against an unfamiliar effect before playing it.

Parameters

  • identity string — The effect's canonical identity, or a short name.

Returns { [string]: any }{ identity, family, summary, cost, params }.

local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)

typed/builtin//modules/api/engine/effects/effects/drain

effects.drain() -> { [string]: number }

Free every backend the pool is holding idle. The pool keeps what it has leased for as long as the engine runs — that is what makes repeated firing cost nothing after the first — and this is the one call that gives it back. A backend a live play still holds is left to that play's own end.

Returns { [string]: number }{ freed, kept }.

print(effects.drain().freed)

typed/builtin//modules/api/engine/effects/effects/families

effects.families() -> { string }

Every family the effects in this world declare, sorted — the values list { family = … } filters on. An effect declaring no family is not one of them.

Returns { string } — Array of family names.

for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end

typed/builtin//modules/api/engine/effects/effects/list

effects.list(opts: table?) -> { string }

The canonical identity of every effect this world can play, sorted. These are the exact strings play takes. Pass { family = "combat" } to get only the effects of one family — the catalogue filtered the way an effect declares itself.

Parameters

  • opts table (optional){ family? = string }. A family is matched without regard to case.

Returns { string } — Array of identities.

for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end

typed/builtin//modules/api/engine/effects/effects/observe

effects.observe() -> { [string]: any }

What the runtime is holding and driving right now — every live play with the reason it is silent when it is, plus what the pool has leased out and what it is keeping idle, in instances and in GPU bytes. This is how a caller and a test tell a working effect from a silent one, and how they tell a pool warming to a wider burst from something leaking: the pool is sized by the most effects it has had to cover at once, which peakLive and peakLeased report beside the current totals.

Returns { [string]: any } — The observation.

local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers

typed/builtin//modules/api/engine/effects/effects/play

effects.play(identity: string, opts: table?) -> any

Play an effect once at a world position. The effect allocates what it needs from the shared pool, draws itself, and gives everything back when it ends — with no update loop on the caller's side.

typed/builtin//modules/api/engine/effects/effects/playOn

effects.playOn(identity: string, target: any?, opts: table?) -> any

Play an effect on an entity: it starts where the entity stands and ends if the entity leaves the world. Move it with the entity by calling handle:retarget(theEntity) as it goes.

Parameters

  • identity string — The effect's canonical identity, or a short name.
  • target any (optional) — An entity proxy or entity id.
  • opts table (optional) — The same options play takes; position is read from the entity.

Returns any — The play handle.

local h = effects.playOn("explosion", drum, { params = { scale = 3 } })

typed/builtin//modules/api/engine/effects/effects/registerBackend

effects.registerBackend(kind: string, backend: table)

Register a new way of drawing under a kind name, so an effect family that needs one the runtime does not ship adds it rather than widening the runtime. Every effect reaches it through ctx.lease(kind, spec).

Parameters

  • kind string — The kind name a spec asks for.
  • backend table — The backend — key, acquire, seat, start, stop, quiet, place, bytes, active, silence and free.
effects.registerBackend("ribbonTrail", myBackend)

typed/builtin//modules/api/engine/effects/effects/silenceReasons

effects.silenceReasons() -> { { reason: string, means: string } }

The closed set of reasons a play can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every reason an observation reports is one of these.

Returns { { reason: string, means: string } } — Array of { reason, means }.

for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end
  • api
  • reference