Log inGet started

entity

Updated 5 September 2026

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

  • type AssetRef — Component identity
  • data table (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

  • type AssetRef (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 of entity.batchSpawn or entity.findAll).
  • type_name string — Component type to add to every entity.
  • data table (optional) — Init data table, applied identically to every entity — the same shape the second arg to entity(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 of entity.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. target accepts an entity-id array or a ecs.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 from ecs.bindEntities(ids).
  • component string (optional) — Component type name (e.g. "Transform").
  • field string (optional) — Field name (e.g. "position").
  • sink buffer (optional) — Typed CPU buffer from substrate.createBuffer({...}) to memcpy field values into. Required when target is 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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to read.
  • buffer number — 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

  • count number — How many entities to spawn (capped at 1,000,000).
  • name_prefix string (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; values is an array the same length as ids (nil slots are skipped). Use for one-shot writes.
  • entity.batchWrite(binding, comp, field, values) — binding handle from ecs.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 from ecs.bindEntities(ids).
  • component string — Component type name.
  • field string — Field name to write.
  • source { any? } | buffer — Per-entity values array (nil entries are skipped), or a typed CPU buffer from substrate.createBuffer({...}). A buffer source requires a binding target.

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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — 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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to write.
  • buffer number — Buffer id from substrate.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

  • target string | 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

  • sourceId string | entityRef — Entity id or entity proxy of the source entity to clone.
  • name string (optional) — Display name for the copy (defaults to source name + " (copy)").
  • opts table (optional){ includeTemporary?: boolean, name?: string }name is the same field the name argument 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

  • idOrProxy string | 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

  • nameOrGlob string — 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

  • nameOrGlob string (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

  • id string | 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

  • id string | 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

  • id string | 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

  • value any — 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

  • handle number — Template handle from entity.template.
  • count number — 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

  • value any — 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

  • value any — 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.
  • opts SpawnOpts (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.
  • opts SpawnOpts (optional) — Same options as entity.spawn (synced is 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

  • def EntityTemplateDef — Template definition: components / ecs / temporary / active / internal / attributes. Per-instance name / position / rotation / scale / parent and any override go through the instantiate callback.

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 of entity.batchSpawn or entity.findAll).
  • type_name string — Component type to add to every entity.
  • data table? (optional) — Init data table, applied identically to every entity — the same shape the second arg to entity(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 of entity.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. target accepts an entity-id array or a ecs.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 from ecs.bindEntities(ids).
  • component string? (optional) — Component type name (e.g. "Transform").
  • field string? (optional) — Field name (e.g. "position").
  • sink buffer? (optional) — Typed CPU buffer from substrate.createBuffer({...}) to memcpy field values into. Required when target is 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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to read.
  • buffer number — 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

  • count number — How many entities to spawn (capped at 1,000,000).
  • name_prefix string? (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; values is an array the same length as ids (nil slots are skipped). Use for one-shot writes.
  • entity.batchWrite(binding, comp, field, values) — binding handle from ecs.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 from ecs.bindEntities(ids).
  • component string — Component type name.
  • field string — Field name to write.
  • source { any? } | buffer — Per-entity values array (nil entries are skipped), or a typed CPU buffer from substrate.createBuffer({...}). A buffer source requires a binding target.
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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — 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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to write.
  • buffer number — Buffer id from substrate.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

  • target string | 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

  • sourceId string | entityRef — Entity id or entity proxy of the source entity to clone.
  • name string? (optional) — Display name for the copy (defaults to source name + " (copy)").
  • opts table? (optional){ includeTemporary?: boolean, name?: string }name is the same field the name argument 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

  • idOrProxy string | 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

  • nameOrGlob string — 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

  • nameOrGlob string? (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

  • id string | 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

  • id string | 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

  • id string | 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

  • handle number — Template handle from entity.template.
  • count number — 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.
  • opts SpawnOpts? (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.
  • opts SpawnOpts? (optional) — Same options as entity.spawn (synced is 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

  • def EntityTemplateDef — Template definition: components / ecs / temporary / active / internal / attributes. Per-instance name / position / rotation / scale / parent and any override go through the instantiate callback.
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 })
  • api
  • reference