entity
The entity namespace — 58 functions.
entity/proxy/component/addSynced
entity(id).component.addSynced(type, data?) -> table | nil
Add a component that replicates to all peers. Same as add() — including returning the new component's live public proxy — but forces sync registration even without a sync {} block.
Parameters
typeAssetRef— Component identitydatatable(optional) — Initial property values for the component
Returns table | nil — The new component's live public proxy; nil when deferred or skipped
local health = entity(id).component.addSynced('Health', { hp = 100 })
entity/proxy/component/pending
entity(id).component.pending(type?) -> { {type: string, instance: string?, instanceId: string, fields: { {name: string, spec: string, value: string} }} }
The components on this entity whose awake() is held back waiting for an asset field to register, and for each one the field, its declared spec, and the identity string that did not resolve. A component in this list is attached and idle: it renders nothing and its methods return nothing until the asset arrives. Empty when every component on the entity has run. Ask here when a component appears to have done nothing — the answer separates an asset still arriving from an identity that names no asset.
Parameters
typeAssetRef(optional) — Component identity to narrow the answer to
Returns table — Array of { type, instance?, instanceId, fields } records; empty when nothing is waiting
local waiting = entity(id).component.pending('Terrain')
if #waiting > 0 then print(waiting[1].fields[1].name, waiting[1].fields[1].value) end
globals/entity/__batchReadIds
entity.__batchReadIds()
Read a component field across many entities by id, in one crossing.
globals/entity/__batchReadToBuffer
entity.__batchReadToBuffer()
Read a component field into a typed substrate buffer.
globals/entity/__batchWriteBound
entity.__batchWriteBound()
Write a component field across a binding handle, in one crossing.
globals/entity/__batchWriteFromBuffer
entity.__batchWriteFromBuffer()
Write a component field from a typed substrate buffer.
globals/entity/__batchWriteIds
entity.__batchWriteIds()
Write a component field across many entities by id, in one crossing.
globals/entity/batchAddComponent
entity.batchAddComponent(targets: { string | entityRef }, type_name: string, data: table?) -> number
Add the same component type to many entities in one call. Returns the count of entities the component was added to — an entity already carrying an unnamed instance of the same type is skipped rather than double-added.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies (e.g. the return ofentity.batchSpawnorentity.findAll).type_namestring— Component type to add to every entity.datatable(optional) — Init data table, applied identically to every entity — the same shape the second arg toentity(id).component.add(type, data)takes.
Returns number — How many entities had the component added.
local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 })
globals/entity/batchDespawn
entity.batchDespawn(targets: { string | entityRef }) -> number
Despawn many entities in one call. Locked or unresolvable entities are skipped. Returns the count queued for despawn.
Parameters
targets{ string | entityRef }— Array of entity ids, entity proxies, or display names (e.g. the return ofentity.batchSpawn/entity.findAll).
Returns number — Count of entities queued for despawn.
local n = entity.batchDespawn(ids)
globals/entity/batchProxy
entity.batchProxy(targets: { string | entityRef }) -> { entityRef? }
Resolve an array of entity ids to proxies in one call. Each output
slot is the standard entity(id) proxy; ids missing from the frame
cache surface as nil at that index. Use when iterating over a snapshot
of entities so per-id lookups don't dominate the hot path.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies.
Returns { entityRef? } — Array of proxies (nil for missing ids).
local proxies = entity.batchProxy(ids)
globals/entity/batchRead
entity.batchRead(target: { string | entityRef } | binding, component: string?, field: string?, sink: buffer?) -> { any? } | number
Read a component-field across many entities in one call.
Polymorphic on the shape of target and sink:
entity.batchRead(ids)/(ids, comp)/(ids, comp, field)— returns one value per entity (a whole snapshot, one component table, or one field value). Missing entities/components/fields surface as nil at that slot.entity.batchRead(binding, comp, field, buffer)— reads each entity's field directly into a typed CPU substrate buffer (substrate.createBuffer({type="vec3"}), etc.) with no per-entity Lua table allocation. Returns the count of successful reads.targetaccepts an entity-id array or aecs.bindEntities(ids)handle. Buffer sinks require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring(optional) — Component type name (e.g. "Transform").fieldstring(optional) — Field name (e.g. "position").sinkbuffer(optional) — Typed CPU buffer fromsubstrate.createBuffer({...})to memcpy field values into. Required whentargetis a binding.
Returns { any? } | number — Per-entity values when reading into Lua tables; count of reads when reading into a buffer.
local snapshot = entity.batchRead(ids)
local positions = entity.batchRead(ids, "Transform", "position")
globals/entity/batchReadToBuffer
entity.batchReadToBuffer(binding: number, component: string, field: string, buffer: number) -> number
FFI primitive backing entity.batchRead(binding, ..., buffer).
Prefer the unified entity.batchRead, which auto-dispatches by
argument shape. Reads each entity's component field directly into a
typed CPU substrate buffer, with no per-entity Lua table allocation.
After the call, read the buffer via buf:read(0, count*stride).
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to read.buffernumber— Destination buffer id (must be the matching type).
Returns number — Count of successful reads.
entity.batchReadToBuffer(binding.id, "Transform", "position", buf.id)
globals/entity/batchSpawn
entity.batchSpawn(count: number, name_prefix: string?) -> { string }
Spawn count entities in one call. Returns an array of the new
entity ids in spawn order. Each entity is given a display name of
<name_prefix><i> (or entity<i> if the prefix is omitted). Prefer this
over looping entity.spawn when creating large entity counts.
Parameters
countnumber— How many entities to spawn (capped at 1,000,000).name_prefixstring(optional) — Display-name prefix appended with the 1-based index. Defaults to "entity".
Returns { string } — Array of newly-spawned entity ids.
local ids = entity.batchSpawn(100, "grass_")
globals/entity/batchWrite
entity.batchWrite(target: { string | entityRef } | binding, component: string, field: string, source: { any? } | buffer) -> number
Write a single component-field across many entities in one call.
Polymorphic on the shape of target and source:
entity.batchWrite(ids, comp, field, values)— per-call entity-id resolution;valuesis an array the same length asids(nil slots are skipped). Use for one-shot writes.entity.batchWrite(binding, comp, field, values)— binding handle fromecs.bindEntities(ids); skips per-call id resolution. Use for per-frame writes against a stable entity set.entity.batchWrite(binding, comp, field, buffer)— typed CPU buffer source (substrate.createBuffer({type="vec3"}), etc.), with no per-entity table allocation. Returns the count of successful writes. Buffer sources require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring— Component type name.fieldstring— Field name to write.source{ any? } | buffer— Per-entity values array (nil entries are skipped), or a typed CPU buffer fromsubstrate.createBuffer({...}). A buffer source requires a bindingtarget.
Returns number — Count of successful writes.
entity.batchWrite(ids, "Transform", "position", positions)
globals/entity/batchWriteBound
entity.batchWriteBound(binding: number, component: string, field: string, values: { any? }) -> number
FFI primitive backing entity.batchWrite(binding, ...) with a
per-entity values table. Prefer the unified entity.batchWrite, which
auto-dispatches by argument shape; this entry stays for power users /
debug code that wants to skip dispatch overhead.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to write.values{ any? }— Per-entity source values (nil = skip). Length must match the binding's entity count.
Returns number — Count of successful writes.
entity.batchWriteBound(binding.id, "Transform", "position", values)
globals/entity/batchWriteFromBuffer
entity.batchWriteFromBuffer(binding: number, component: string, field: string, buffer: number) -> number
FFI primitive backing entity.batchWrite(binding, ..., buffer).
Prefer the unified entity.batchWrite, which auto-dispatches by
argument shape. Caller fills a typed substrate buffer
(substrate.createBuffer({type="vec3"})) once via buf:write(...),
then this memcpys 12 (vec3) or 16 (quat) bytes per entity into the
component field. Buffer count and binding count should match — a
mismatch processes the smaller of the two.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to write.buffernumber— Buffer id fromsubstrate.createBuffer({type="vec3", len=N}).id.
Returns number — Count of successful writes.
entity.batchWriteFromBuffer(binding.id, "Transform", "position", buf.id)
globals/entity/capture
entity.capture(builder: () -> ()) -> ({ string }, any?, { string })
Run builder inside an entity capture scope and return the entity ids
it minted, in creation order, the error it raised (if any), and the ids
among them that a component the builder attached minted in its own
lifecycle. Every id minted while the builder runs is recorded — through
entity.spawn, entity.spawnSynced, entity.batchSpawn, and
entity.instantiate alike. Scopes nest: an id minted inside an inner
capture is recorded by that capture AND every enclosing one — the
innermost capture answers, so a nested build shapes its own entities,
not the ones around it. A builder that raises still returns its ids, so
the caller can despawn what a failed build left behind; the scope closes
either way and never outlives this call. While the builder runs, an
operation whose result cannot be composed into a record is refused
rather than applied, and so is any operation aimed at an entity the
builder did not mint — a builder that returned while something it did
was refused comes back with an error naming every refusal.
Parameters
builder() -> ()— Function run inside the scope; the entities it creates are what comes back.
Returns ({ string }, any?, { string }) — Entity ids minted while the builder ran, in creation order; the error it raised (or the refusals it hit), or nil; and the ids a component the builder attached minted in its own lifecycle.
local ids, err, reproduced = entity.capture(function() entity.spawn("chair") end)
globals/entity/despawn
entity.despawn(target: string | entityRef)
Despawn an entity and all its components. Pass an id string or an entity proxy to despawn that ONE entity. Pass a name to despawn EVERY entity with that name — names are not unique, so a name argument despawns all matches, not one arbitrary match. A despawned id becomes invalid after this call. Raises if no entity matches; for a bulk name despawn, locked entities are skipped with a logged summary and only raise if every match is locked.
Parameters
targetstring | entityRef— Entity id, name, or entity proxy. A name despawns all entities sharing that name.
entity.despawn(id)
entity.despawn("Enemy") -- despawns every entity named "Enemy"
globals/entity/duplicate
entity.duplicate(sourceId: string | entityRef, name: string?, opts: table?) -> string?
Duplicate an entity with all its components (transform, script
components, attributes, visuals, material) and its descendants. Returns
the new entity's id, or nil when sourceId names no live entity.
Descendants marked temporary are left out of the copy: they are
scaffolding whatever spawned them re-creates, so a component that
regenerates its own children rebuilds them on the copy rather than the
copy carrying a second set. includeTemporary copies them too, for the
hierarchy that IS the temporary thing.
Parameters
sourceIdstring | entityRef— Entity id or entity proxy of the source entity to clone.namestring(optional) — Display name for the copy (defaults to source name + " (copy)").optstable(optional) —{ includeTemporary?: boolean, name?: string }—nameis the same field thenameargument sets, and wins when both are given.
Returns string? — The new entity's id, or nil when the source is not live.
local copyId = entity.duplicate(id); if copyId then entity(copyId).position = { 1, 0, 0 } end
local copyId = entity.duplicate(id, "Turret", { includeTemporary = true })
globals/entity/exists
entity.exists(idOrProxy: string | entityRef) -> boolean
Check whether an entity currently exists in the scene. Accepts an
entity-id string or an entity proxy, matched by entity id — so it agrees
exactly with entity(id). A name is a different kind of identifier: a
string that misses as an id but names a live entity raises rather than
answering false, since false there is indistinguishable from absence.
Check by name with entity.find(name) ~= nil.
Parameters
idOrProxystring | entityRef— Entity id or an entity proxy.
Returns boolean — true if the entity exists.
if entity.exists(id) then ... end
globals/entity/find
entity.find(nameOrGlob: string) -> entityRef?
Find the first entity matching nameOrGlob. A plain string matches
an exact id or Name component; a string containing * (any run of
characters) or ? (any single character) matches Names as a glob, so
entity.find("enemy_*") is the first entity whose name starts with
enemy_. A glob addresses Names only, never ids. Same-frame pending
spawns are searched too, and anything queued for despawn in the same
frame is skipped. Names are NOT unique — use entity.findAll when every
match matters.
Parameters
nameOrGlobstring— Exact entity Name or id, or a*/?glob over Names.
Returns entityRef? — First matching entity proxy, or nil.
local e = entity.find("enemy_*")
globals/entity/findAll
entity.findAll(nameOrGlob: string?) -> { entityRef }
Enumerate entity proxies. With a nameOrGlob argument, returns every
entity whose Name component or id matches (names are not unique): a
plain string matches exactly, while a * / ? glob matches Names. With
no argument, returns every entity in the current snapshot —
findAll("") is the exact-match filter for the empty name, which
normally matches nothing. Same-frame pending spawns are included and
same-frame despawns filtered out. Elements are entity proxies, not id
strings — for ids, wrap the result: entity.ids(entity.findAll(...)).
Parameters
nameOrGlobstring(optional) — Exact entity Name or id to filter by, or a*/?glob over Names. Omit to enumerate every entity.
Returns { entityRef } — Array of entity proxies, possibly empty.
for _, e in entity.findAll("enemy_*") do e:despawn() end
globals/entity/getChildren
entity.getChildren(id: string | entityRef) -> { entityRef }
Get an array of the direct children as entity proxies. Each element
carries .name, .id, .position, .component, and the rest of the
per-entity surface — the same shape entity.findAll returns.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns { entityRef } — Array of child entity proxies, possibly empty.
for _, c in entity.getChildren(id) do c.internal = true end
globals/entity/getDescendants
entity.getDescendants(id: string | entityRef) -> { entityRef }
Get every descendant (children, grandchildren, and deeper) of the given entity as entity proxies in breadth-first order, excluding the entity itself. Resolves the whole subtree in one linear pass over the entity set, so a large subtree costs proportionally to the entity count rather than to the subtree size times the entity count.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns { entityRef } — Array of descendant entity proxies, possibly empty.
local all = entity.getDescendants(id)
globals/entity/getParent
entity.getParent(id: string | entityRef) -> entityRef?
Get the parent entity proxy, or nil if the entity is a root entity.
The returned proxy carries .name, .id, .position, .component,
and the rest of the per-entity surface — the same shape entity.find
returns.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns entityRef? — Parent entity proxy, or nil.
local p = entity.getParent(id)
globals/entity/hierarchy
entity.hierarchy()
Hierarchy helpers reached as entity.hierarchy.<member>.
globals/entity/ids
entity.ids(value: any) -> { string }
Coerce an entity, a list of entities, or nil into a list of entity id strings. Accepts id strings and entityRefs in any mix, so the entity-answering calls and a stored list of ids both feed it. A single entity becomes a one-element list and nil becomes an empty list. Order is preserved and duplicates are kept.
Parameters
valueany— An entity, a list of entities, or nil.
Returns { string } — Array of entity id strings.
local ids = entity.ids(root.getChildren())
local ids = entity.ids(entity.findAll())
globals/entity/instantiate
entity.instantiate(handle: number, count: number, fn: ((number) -> EntityInstantiateOverrides?)?) -> { string }
Spawn count instances of a template registered with
entity.template. Each instance gets a fresh entity id; the optional
fn(i) callback runs per instance (i in 1..=count) and may return an
overrides table. Override keys: name, position, rotation, scale,
parent, temporary / active / internal, attributes, components
(script components, merged over the template body's data for that type
— a type the template lacks is added fresh), and ecs (native
components, merged the same way). Each override supersedes the
template's shared config for that instance. The whole batch crosses in
one call and lands as a single deferred mutation the engine expands
into bulk work — per-instance cost drops from a full round trip to one
callback plus one mutation. Inside queue() the batch is deferred onto
the cross-frame ring; outside, it lands in the next frame's drain.
Returns the array of newly-minted entity ids in spawn order.
Parameters
handlenumber— Template handle fromentity.template.countnumber— Number of instances to spawn (capped at 1,000,000).fn((number) -> EntityInstantiateOverrides?)(optional) — Per-instance override callback(i) -> table?.
Returns { string } — Array of newly-spawned entity ids in spawn order.
local ids = entity.instantiate(h, 50, function(i) return { position = { i, 0, 0 } } end)
globals/entity/proxies
entity.proxies(value: any) -> { entityRef }
Coerce an entity, a list of entities, or nil into a list of entityRefs.
Parameters
valueany— An entity, a list of entities, or nil.
Returns { entityRef } — Array of entityRefs.
globals/entity/refs
entity.refs(value: any) -> { entityRef }
Coerce an entity, a list of entities, or nil into a list of entityRefs —
the form carrying .position, .component, and the rest of the per-entity
surface. Accepts id strings and entityRefs in any mix. A single entity
becomes a one-element list and nil becomes an empty list. An id whose
entity has despawned raises, naming its position.
Parameters
valueany— An entity, a list of entities, or nil.
Returns { entityRef } — Array of entityRefs.
for _, e in entity.refs(savedIds) do e.position = { 0, 1, 0 } end
globals/entity/spawn
entity.spawn(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?) -> entityRef
Spawn a new entity and return its PROXY (the same value entity(id)
yields) — act on it immediately (entity.spawn(name).component.add(...),
.localPosition = ...) with no second entity(id) round trip. The proxy
still exposes .id for the rare site that needs the raw string. An
entity with no components carries only a Transform and is invisible;
pass components to give it the components that make it visible in the
same call — entity.spawn { name = "crate", components = { Model = { model = "cube" } } } — or add them afterwards through the returned
proxy. Mirrors entity.find / entity.findAll, which also return
proxies. The options table can be passed on its own with the name inside
it — entity.spawn { name = "turret", position = { 1, 2, 3 } } is the
same call as entity.spawn("turret", { position = { 1, 2, 3 } }).
Parameters
nameOrOpts(string | SpawnOpts)(optional) — Display name for the entity, or the options table itself.optsSpawnOpts(optional) — Options:components= component types to attach to the new entity, keyed by type name with each value the component's init table (attached in sorted type order; a failing add raises),internal= take the entity out of the default entity listings (it still renders —entity(id):hide()stops the draw),parent= parent entity id or proxy,temporary= skip this entity (and descendants) from scene/world saves,position/rotation/scale= place the entity's Transform at spawn,id= restore a previously-assigned entity id (scene_loader use; leave unset for a normal spawn). An unrecognised key is rejected loudly.
Returns entityRef — Proxy for the new entity (carries .id, .component, transform properties, etc.).
local e = entity.spawn("crate", { components = { Model = { model = "cube" } } })
local e = entity.spawn { name = "turret", position = { 1, 2, 3 } }
globals/entity/spawnSynced
entity.spawnSynced(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?) -> entityRef
Spawn an entity already flagged multiplayer-synced at the root — the
explicit form of entity.spawn for SHARED, host-authoritative content.
The entity's existence broadcasts to every peer; joiners receive it from
the relay snapshot instead of spawning their own copy. Use this (NOT
entity.spawn) for anything that must be the SAME object on all
clients: enemies, pickups, projectiles, dynamic world props. Call it
ONLY where exactly one client runs the code — a scene's onHostLoad
(host-only) phase, or behind multiplayer.isHost(). Calling it in
all-client code makes every client spawn+sync its own copy — the
double-spawn "explosion". Equivalent to
entity.spawn(name, { synced = true }); identical in every other
respect.
Parameters
nameOrOpts(string | SpawnOpts)(optional) — Display name for the entity, or the options table itself.optsSpawnOpts(optional) — Same options asentity.spawn(syncedis already implied).
Returns entityRef — Proxy for the new synced entity.
if multiplayer.isHost() then entity.spawnSynced("Goblin") end
globals/entity/template
entity.template(def: EntityTemplateDef) -> number
Construct a reusable spawn template. Captures a shared entity config
ONCE and returns a stable handle for entity.instantiate(handle, count, fn?) — one call per batch instead of one per entity. def keys:
components (script components, { [type] = init-data }), ecs
(array of native ecs.X{...} components), temporary (instances skip
scene/world saves), active (spawn state), internal (instances are
taken out of the default entity listings; they still render),
attributes ({ key = value } applied to every instance). Every value
is a shared default; a per-instance entity.instantiate override
supersedes it. The template body is captured by value — later edits to
the source table do not affect templates already created.
Parameters
defEntityTemplateDef— Template definition:components/ecs/temporary/active/internal/attributes. Per-instancename/position/rotation/scale/parentand any override go through theinstantiatecallback.
Returns number — Stable template handle for entity.instantiate.
local h = entity.template({ components = { Model = { model = "cube" } } })
globals/entity/tree
entity.tree(opts: { [string]: any }?) -> { [string]: any }
A windowed, lean view over the scene's entity tree, in one crossing.
Rows carry id, name, parentId, depth, childCount, active, sceneLayer and
componentNames — names only, never component values — so the call costs
the rows it answers with rather than the size of the scene. Entities group
under scene layers, per-layer roots and children name-sorted; internal
entities and their subtrees stay out. expanded names the ids whose
children unfold, and a collapsed node still reports its childCount;
filter keeps the rows whose name or id contains the needle plus every
ancestor on a path to one, auto-unfolded, with the actual matches flagged
matched. offset / limit window the flattened rows, layer scopes the
window and its total to one layer while layers still reports every
layer's row count, and revision echoes
getEntitiesRevision("structure"), which moves only on structural change.
Parameters
opts{ [string]: any }(optional) —{ layer?, expanded?, filter?, offset?, limit? }.
Returns { [string]: any } — { rows, total, layers, revision }.
local view = entity.tree({ filter = "crate", limit = 50 })
modules/entity/README
require("@builtin/modules/api/engine/entity") -- entity (also available as global 'entity')
Entity spawn / despawn / query / hierarchy surface. Calling the table itself — entity(idOrProxy) — resolves an id or proxy to its live entityRef proxy. Public Luau surface over the __entity Internal FFI namespace, composed with the entityRef proxy metatable, the hierarchy swap helper, id/proxy coercion, and the polymorphic batch read/write dispatch.
Usage: local entity = require("@builtin/modules/api/engine/entity") Also available as global: entity
modules/entity/batchAddComponent
batchAddComponent(targets: { string | entityRef }, type_name: string, data: table?): number
Add the same component type to many entities in one call. Returns the count of entities the component was added to — an entity already carrying an unnamed instance of the same type is skipped rather than double-added.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies (e.g. the return ofentity.batchSpawnorentity.findAll).type_namestring— Component type to add to every entity.datatable?(optional) — Init data table, applied identically to every entity — the same shape the second arg toentity(id).component.add(type, data)takes.
local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 })
modules/entity/batchDespawn
batchDespawn(targets: { string | entityRef }): number
Despawn many entities in one call. Locked or unresolvable entities are skipped. Returns the count queued for despawn.
Parameters
targets{ string | entityRef }— Array of entity ids, entity proxies, or display names (e.g. the return ofentity.batchSpawn/entity.findAll).
local n = entity.batchDespawn(ids)
modules/entity/batchProxy
batchProxy(targets: { string | entityRef }): { entityRef? }
Resolve an array of entity ids to proxies in one call. Each output
slot is the standard entity(id) proxy; ids missing from the frame
cache surface as nil at that index. Use when iterating over a snapshot
of entities so per-id lookups don't dominate the hot path.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies.
local proxies = entity.batchProxy(ids)
modules/entity/batchRead
batchRead(target: { string | entityRef } | binding, component: string?, field: string?, sink: buffer?): { any? } | number
Read a component-field across many entities in one call.
Polymorphic on the shape of target and sink:
entity.batchRead(ids)/(ids, comp)/(ids, comp, field)— returns one value per entity (a whole snapshot, one component table, or one field value). Missing entities/components/fields surface as nil at that slot.entity.batchRead(binding, comp, field, buffer)— reads each entity's field directly into a typed CPU substrate buffer (substrate.createBuffer({type="vec3"}), etc.) with no per-entity Lua table allocation. Returns the count of successful reads.targetaccepts an entity-id array or aecs.bindEntities(ids)handle. Buffer sinks require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring?(optional) — Component type name (e.g. "Transform").fieldstring?(optional) — Field name (e.g. "position").sinkbuffer?(optional) — Typed CPU buffer fromsubstrate.createBuffer({...})to memcpy field values into. Required whentargetis a binding.
local snapshot = entity.batchRead(ids)
local positions = entity.batchRead(ids, "Transform", "position")
modules/entity/batchReadToBuffer
batchReadToBuffer(binding: number, component: string, field: string, buffer: number): number
FFI primitive backing entity.batchRead(binding, ..., buffer).
Prefer the unified entity.batchRead, which auto-dispatches by
argument shape. Reads each entity's component field directly into a
typed CPU substrate buffer, with no per-entity Lua table allocation.
After the call, read the buffer via buf:read(0, count*stride).
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to read.buffernumber— Destination buffer id (must be the matching type).
entity.batchReadToBuffer(binding.id, "Transform", "position", buf.id)
modules/entity/batchSpawn
batchSpawn(count: number, name_prefix: string?): { string }
Spawn count entities in one call. Returns an array of the new
entity ids in spawn order. Each entity is given a display name of
<name_prefix><i> (or entity<i> if the prefix is omitted). Prefer this
over looping entity.spawn when creating large entity counts.
Parameters
countnumber— How many entities to spawn (capped at 1,000,000).name_prefixstring?(optional) — Display-name prefix appended with the 1-based index. Defaults to "entity".
local ids = entity.batchSpawn(100, "grass_")
modules/entity/batchWrite
batchWrite(target: { string | entityRef } | binding, component: string, field: string, source: { any? } | buffer): number
Write a single component-field across many entities in one call.
Polymorphic on the shape of target and source:
entity.batchWrite(ids, comp, field, values)— per-call entity-id resolution;valuesis an array the same length asids(nil slots are skipped). Use for one-shot writes.entity.batchWrite(binding, comp, field, values)— binding handle fromecs.bindEntities(ids); skips per-call id resolution. Use for per-frame writes against a stable entity set.entity.batchWrite(binding, comp, field, buffer)— typed CPU buffer source (substrate.createBuffer({type="vec3"}), etc.), with no per-entity table allocation. Returns the count of successful writes. Buffer sources require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring— Component type name.fieldstring— Field name to write.source{ any? } | buffer— Per-entity values array (nil entries are skipped), or a typed CPU buffer fromsubstrate.createBuffer({...}). A buffer source requires a bindingtarget.
entity.batchWrite(ids, "Transform", "position", positions)
modules/entity/batchWriteBound
batchWriteBound(binding: number, component: string, field: string, values: { any? }): number
FFI primitive backing entity.batchWrite(binding, ...) with a
per-entity values table. Prefer the unified entity.batchWrite, which
auto-dispatches by argument shape; this entry stays for power users /
debug code that wants to skip dispatch overhead.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to write.values{ any? }— Per-entity source values (nil = skip). Length must match the binding's entity count.
entity.batchWriteBound(binding.id, "Transform", "position", values)
modules/entity/batchWriteFromBuffer
batchWriteFromBuffer(binding: number, component: string, field: string, buffer: number): number
FFI primitive backing entity.batchWrite(binding, ..., buffer).
Prefer the unified entity.batchWrite, which auto-dispatches by
argument shape. Caller fills a typed substrate buffer
(substrate.createBuffer({type="vec3"})) once via buf:write(...),
then this memcpys 12 (vec3) or 16 (quat) bytes per entity into the
component field. Buffer count and binding count should match — a
mismatch processes the smaller of the two.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to write.buffernumber— Buffer id fromsubstrate.createBuffer({type="vec3", len=N}).id.
entity.batchWriteFromBuffer(binding.id, "Transform", "position", buf.id)
modules/entity/capture
capture(builder: () -> ()): ({ string }, any?, { string })
Run builder inside an entity capture scope and return the entity ids
it minted, in creation order, the error it raised (if any), and the ids
among them that a component the builder attached minted in its own
lifecycle. Every id minted while the builder runs is recorded — through
entity.spawn, entity.spawnSynced, entity.batchSpawn, and
entity.instantiate alike. Scopes nest: an id minted inside an inner
capture is recorded by that capture AND every enclosing one — the
innermost capture answers, so a nested build shapes its own entities,
not the ones around it. A builder that raises still returns its ids, so
the caller can despawn what a failed build left behind; the scope closes
either way and never outlives this call. While the builder runs, an
operation whose result cannot be composed into a record is refused
rather than applied, and so is any operation aimed at an entity the
builder did not mint — a builder that returned while something it did
was refused comes back with an error naming every refusal.
Parameters
builder() -> ()— Function run inside the scope; the entities it creates are what comes back.
local ids, err, reproduced = entity.capture(function() entity.spawn("chair") end)
modules/entity/despawn
despawn(target: string | entityRef)
Despawn an entity and all its components. Pass an id string or an entity proxy to despawn that ONE entity. Pass a name to despawn EVERY entity with that name — names are not unique, so a name argument despawns all matches, not one arbitrary match. A despawned id becomes invalid after this call. Raises if no entity matches; for a bulk name despawn, locked entities are skipped with a logged summary and only raise if every match is locked.
Parameters
targetstring | entityRef— Entity id, name, or entity proxy. A name despawns all entities sharing that name.
entity.despawn(id)
entity.despawn("Enemy") -- despawns every entity named "Enemy"
modules/entity/duplicate
duplicate(sourceId: string | entityRef, name: string?, opts: table?): string?
Duplicate an entity with all its components (transform, script
components, attributes, visuals, material) and its descendants. Returns
the new entity's id, or nil when sourceId names no live entity.
Descendants marked temporary are left out of the copy: they are
scaffolding whatever spawned them re-creates, so a component that
regenerates its own children rebuilds them on the copy rather than the
copy carrying a second set. includeTemporary copies them too, for the
hierarchy that IS the temporary thing.
Parameters
sourceIdstring | entityRef— Entity id or entity proxy of the source entity to clone.namestring?(optional) — Display name for the copy (defaults to source name + " (copy)").optstable?(optional) —{ includeTemporary?: boolean, name?: string }—nameis the same field thenameargument sets, and wins when both are given.
local copyId = entity.duplicate(id); if copyId then entity(copyId).position = { 1, 0, 0 } end
local copyId = entity.duplicate(id, "Turret", { includeTemporary = true })
modules/entity/exists
exists(idOrProxy: string | entityRef): boolean
Check whether an entity currently exists in the scene. Accepts an
entity-id string or an entity proxy, matched by entity id — so it agrees
exactly with entity(id). A name is a different kind of identifier: a
string that misses as an id but names a live entity raises rather than
answering false, since false there is indistinguishable from absence.
Check by name with entity.find(name) ~= nil.
Parameters
idOrProxystring | entityRef— Entity id or an entity proxy.
if entity.exists(id) then ... end
modules/entity/find
find(nameOrGlob: string): entityRef?
Find the first entity matching nameOrGlob. A plain string matches
an exact id or Name component; a string containing * (any run of
characters) or ? (any single character) matches Names as a glob, so
entity.find("enemy_*") is the first entity whose name starts with
enemy_. A glob addresses Names only, never ids. Same-frame pending
spawns are searched too, and anything queued for despawn in the same
frame is skipped. Names are NOT unique — use entity.findAll when every
match matters.
Parameters
nameOrGlobstring— Exact entity Name or id, or a*/?glob over Names.
local e = entity.find("enemy_*")
modules/entity/findAll
findAll(nameOrGlob: string?): { entityRef }
Enumerate entity proxies. With a nameOrGlob argument, returns every
entity whose Name component or id matches (names are not unique): a
plain string matches exactly, while a * / ? glob matches Names. With
no argument, returns every entity in the current snapshot —
findAll("") is the exact-match filter for the empty name, which
normally matches nothing. Same-frame pending spawns are included and
same-frame despawns filtered out. Elements are entity proxies, not id
strings — for ids, wrap the result: entity.ids(entity.findAll(...)).
Parameters
nameOrGlobstring?(optional) — Exact entity Name or id to filter by, or a*/?glob over Names. Omit to enumerate every entity.
for _, e in entity.findAll("enemy_*") do e:despawn() end
modules/entity/getChildren
getChildren(id: string | entityRef): { entityRef }
Get an array of the direct children as entity proxies. Each element
carries .name, .id, .position, .component, and the rest of the
per-entity surface — the same shape entity.findAll returns.
Parameters
idstring | entityRef— Entity id or entity proxy.
for _, c in entity.getChildren(id) do c.internal = true end
modules/entity/getDescendants
getDescendants(id: string | entityRef): { entityRef }
Get every descendant (children, grandchildren, and deeper) of the given entity as entity proxies in breadth-first order, excluding the entity itself. Resolves the whole subtree in one linear pass over the entity set, so a large subtree costs proportionally to the entity count rather than to the subtree size times the entity count.
Parameters
idstring | entityRef— Entity id or entity proxy.
local all = entity.getDescendants(id)
modules/entity/getParent
getParent(id: string | entityRef): entityRef?
Get the parent entity proxy, or nil if the entity is a root entity.
The returned proxy carries .name, .id, .position, .component,
and the rest of the per-entity surface — the same shape entity.find
returns.
Parameters
idstring | entityRef— Entity id or entity proxy.
local p = entity.getParent(id)
modules/entity/instantiate
instantiate(handle: number, count: number, fn: ((number) -> EntityInstantiateOverrides?)?): { string }
Spawn count instances of a template registered with
entity.template. Each instance gets a fresh entity id; the optional
fn(i) callback runs per instance (i in 1..=count) and may return an
overrides table. Override keys: name, position, rotation, scale,
parent, temporary / active / internal, attributes, components
(script components, merged over the template body's data for that type
— a type the template lacks is added fresh), and ecs (native
components, merged the same way). Each override supersedes the
template's shared config for that instance. The whole batch crosses in
one call and lands as a single deferred mutation the engine expands
into bulk work — per-instance cost drops from a full round trip to one
callback plus one mutation. Inside queue() the batch is deferred onto
the cross-frame ring; outside, it lands in the next frame's drain.
Returns the array of newly-minted entity ids in spawn order.
Parameters
handlenumber— Template handle fromentity.template.countnumber— Number of instances to spawn (capped at 1,000,000).fn((number) -> EntityInstantiateOverrides?)?(optional) — Per-instance override callback(i) -> table?.
local ids = entity.instantiate(h, 50, function(i) return { position = { i, 0, 0 } } end)
modules/entity/spawn
spawn(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?): entityRef
Spawn a new entity and return its PROXY (the same value entity(id)
yields) — act on it immediately (entity.spawn(name).component.add(...),
.localPosition = ...) with no second entity(id) round trip. The proxy
still exposes .id for the rare site that needs the raw string. An
entity with no components carries only a Transform and is invisible;
pass components to give it the components that make it visible in the
same call — entity.spawn { name = "crate", components = { Model = { model = "cube" } } } — or add them afterwards through the returned
proxy. Mirrors entity.find / entity.findAll, which also return
proxies. The options table can be passed on its own with the name inside
it — entity.spawn { name = "turret", position = { 1, 2, 3 } } is the
same call as entity.spawn("turret", { position = { 1, 2, 3 } }).
Parameters
nameOrOpts(string | SpawnOpts)?(optional) — Display name for the entity, or the options table itself.optsSpawnOpts?(optional) — Options:components= component types to attach to the new entity, keyed by type name with each value the component's init table (attached in sorted type order; a failing add raises),internal= take the entity out of the default entity listings (it still renders —entity(id):hide()stops the draw),parent= parent entity id or proxy,temporary= skip this entity (and descendants) from scene/world saves,position/rotation/scale= place the entity's Transform at spawn,id= restore a previously-assigned entity id (scene_loader use; leave unset for a normal spawn). An unrecognised key is rejected loudly.
local e = entity.spawn("crate", { components = { Model = { model = "cube" } } })
local e = entity.spawn { name = "turret", position = { 1, 2, 3 } }
modules/entity/spawnSynced
spawnSynced(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?): entityRef
Spawn an entity already flagged multiplayer-synced at the root — the
explicit form of entity.spawn for SHARED, host-authoritative content.
The entity's existence broadcasts to every peer; joiners receive it from
the relay snapshot instead of spawning their own copy. Use this (NOT
entity.spawn) for anything that must be the SAME object on all
clients: enemies, pickups, projectiles, dynamic world props. Call it
ONLY where exactly one client runs the code — a scene's onHostLoad
(host-only) phase, or behind multiplayer.isHost(). Calling it in
all-client code makes every client spawn+sync its own copy — the
double-spawn "explosion". Equivalent to
entity.spawn(name, { synced = true }); identical in every other
respect.
Parameters
nameOrOpts(string | SpawnOpts)?(optional) — Display name for the entity, or the options table itself.optsSpawnOpts?(optional) — Same options asentity.spawn(syncedis already implied).
if multiplayer.isHost() then entity.spawnSynced("Goblin") end
modules/entity/template
template(def: EntityTemplateDef): number
Construct a reusable spawn template. Captures a shared entity config
ONCE and returns a stable handle for entity.instantiate(handle, count, fn?) — one call per batch instead of one per entity. def keys:
components (script components, { [type] = init-data }), ecs
(array of native ecs.X{...} components), temporary (instances skip
scene/world saves), active (spawn state), internal (instances are
taken out of the default entity listings; they still render),
attributes ({ key = value } applied to every instance). Every value
is a shared default; a per-instance entity.instantiate override
supersedes it. The template body is captured by value — later edits to
the source table do not affect templates already created.
Parameters
defEntityTemplateDef— Template definition:components/ecs/temporary/active/internal/attributes. Per-instancename/position/rotation/scale/parentand any override go through theinstantiatecallback.
local h = entity.template({ components = { Model = { model = "cube" } } })
modules/entity/tree
tree(opts: { [string]: any }?): { [string]: any }
A windowed, lean view over the scene's entity tree, in one crossing.
Rows carry id, name, parentId, depth, childCount, active, sceneLayer and
componentNames — names only, never component values — so the call costs
the rows it answers with rather than the size of the scene. Entities group
under scene layers, per-layer roots and children name-sorted; internal
entities and their subtrees stay out. expanded names the ids whose
children unfold, and a collapsed node still reports its childCount;
filter keeps the rows whose name or id contains the needle plus every
ancestor on a path to one, auto-unfolded, with the actual matches flagged
matched. offset / limit window the flattened rows, layer scopes the
window and its total to one layer while layers still reports every
layer's row count, and revision echoes
getEntitiesRevision("structure"), which moves only on structural change.
Parameters
opts{ [string]: any }?(optional) —{ layer?, expanded?, filter?, offset?, limit? }.
local view = entity.tree({ filter = "crate", limit = 50 })