Entities
An entity is a named handle with a stable id, a transform, and a place in a hierarchy. On its own it does nothing — it has no shape, no physics, no behaviour. Every capability comes from the…
local e = entity.spawn("lantern") -- a bare entity; returns its proxy, ready to drive
local id = e.id -- the stable id string ("ent_…"), for when you need it
local e2 = entity(id) -- look a proxy up later by id only (entity.find(name) resolves names)
Inside a component, public.entity is this same proxy, already bound to the owning entity — so everything below works the same whether you spawned the entity or you're reacting to it from a component.
The proxy is sealed: reading or calling a name that doesn't exist raises a clear error that names the bad access and lists what's valid (EntityProxy: no property/method 'fovv'… Valid: …) — it never silently returns nil, so typos fail loud. The asset, component, and camera handles behave the same way.
Identity: id vs name
Every entity has an id (ent_…, stable and unique — this is what persists and what other entities reference) and a name (human-readable, not unique). entity("ent_…") looks one up exactly; entity.find("lantern") returns the first proxy with that name, or nil. Names are for you; ids are for the engine.
The transform
Position and rotation default to world space; prefix local for parent-relative. Scale is always local.
e.position = { 0, 2, 0 } -- world position
e.localPosition = { 0, 1, 0 } -- relative to parent
e.rotation.eulerAngles = { 0, 90, 0 } -- degrees
e.localScale = { 2, 2, 2 }
local p = e.position -- read back: p.x, p.y, p.z
The proxy reads are snapshots at the moment of the call — for a tight loop, read once into a local rather than re-reading e.position each iteration.
Hierarchy
Entities form a tree. Parenting keeps the child's world transform, so attaching something doesn't teleport it.
local body = entity.spawn("car")
local wheel = entity.spawn("wheel", { parent = body }) -- parented at spawn
wheel.setParent(body) -- or later
wheel.getParent() -- parent id, or nil
body.getChildren() -- { childId, … }
wheel.unparent() -- becomes a root
Components are where the work lives
Add capabilities by adding components; read and change them through the same proxy. The everyday way to give an entity a real, recognizable form is to reference an asset that packages one — a bundle (mesh + material + rig) — and spawn it, then work with its components:
asset.list("bundle") -- survey: @builtin::avatars.humanoid, …
tools.use("entityOps", "fromAsset", "@builtin::avatars.humanoid", { name = "guard" }) -- reference one and spawn it
local guard = entity.find("guard")
guard.component.add("Physics") -- add behaviour
guard.component.has("Physics") -- boolean
guard.component.list() -- what's attached
guard.component.remove("Physics")
A Model component attaches a single mesh + material to a bare entity. The mesh + material are real assets — found in the library, generated, or authored:
e.component.add("Model", { model = "@builtin::avatars.humanoid" }) -- a real asset reference
Survey real content with asset.list("model") / asset.list("bundle") and the ZeroMind library first (the asset-system and worlds guides); when nothing fits, generate it (generating-assets-and-content). What a component is, its fields, and its lifecycle are in the components guide.
Active, visible, temporary — three independent ideas
| You want… | Use | Effect |
|---|---|---|
| To disable an entity and its subtree (no ticks, no render) | e.setActive(false) / read e.active, e.activeInHierarchy | Whole-entity on/off; folds in parent state |
| To stop drawing it but keep it live | e.hide() / e.show() | Rendering only; it still exists and ticks |
| To keep it out of the saved world | e.setTemporary(true) / read e.temporary | Excluded from serialization (with its descendants); still live at runtime |
Reach for the one that matches your intent — "disabled", "invisible", and "not saved" are genuinely different states.
Locks — why some entities refuse to be removed
An entity (or one of its components) can be locked to refuse destructive operations. Locks are why a despawn or a component.remove sometimes comes back refused rather than silently doing nothing:
e.lock("entity-destroy") -- refuse despawn of this entity
e.locked -- boolean: is anything locked on it?
e.locks() -- { destroy = bool, components = { ["<type>"] = { remove, write } } }
e.unlock("entity-destroy") -- lift the lock
e.component.lock("Model", "component-remove") -- per-component: "component-remove" / "component-write"
The engine locks the state it depends on — notably the player identity entity, which is destroy-locked and whose UserIdentity component is remove-locked. So entity.despawn(player) (or removing its UserIdentity component) comes back refused with a message telling you to :unlock() first — that's the engine protecting load-bearing state, not a bug. Unlock deliberately only when you truly mean to.
A shared, multiplayer world
The world is live and shared with everyone connected (see the engine and multiplayer guides). An entity tracks its ownership, and you choose which entities replicate:
e.setSynced(true) -- this entity replicates to other peers
e.synced() -- boolean — is it replicated?
e.isLocal() -- boolean — does this peer own it?
e.owner() -- owning peer id (0 = unowned/server)
Author with that in mind: gameplay entities that everyone should see get setSynced(true); per-player scratch can stay local.
Finding entities
entity.find("lantern") -- first proxy with that name, or nil
entity.findAll("lantern") -- every match, as proxies
entity.exists(id) -- boolean
queryEntitiesTable() -- one cached snapshot of every entity { id, name, parentId, components }
queryEntitiesTable() is the cheap way to scan everything at once; entity(id)/find are for working with a specific entity.
The rest
The proxy carries more than this — attributes (a per-entity key/value bag via e.attribute), bone attachment, session scoping, duplication. The proxy's full method set is discoverable: read modules/api/engine source, or call and inspect (lsp.methods/lsp.describe, the discovering guide). The model above — id + transform + hierarchy + components, in a shared world — is the part you can't grep for.