Log inGet started

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…UseEffect
To disable an entity and its subtree (no ticks, no render)e.setActive(false) / read e.active, e.activeInHierarchyWhole-entity on/off; folds in parent state
To stop drawing it but keep it livee.hide() / e.show()Rendering only; it still exists and ticks
To keep it out of the saved worlde.setTemporary(true) / read e.temporaryExcluded 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.

  • documentation
  • guide