---
title: "Entities"
description: "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…"
section: "Core"
slug: "core-entities"
canonical: "https://origozero.ai/docs/core-entities"
updated: "2026-09-04T19:42:51.710122056+00:00"
tags: ["documentation", "guide"]
---

# Entities

```lua
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.

```lua
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.

Each of these values reads its components off the entity when you ask, so one held past its entity's destruction answers with a marker instead of numbers — see *A proxy whose entity is gone* below.

### Turning about an axis

To face an entity along an angle, name the axis to turn about and the angle to
turn. `setAxisAngle` states the rotation outright; `rotateAxisAngle` adds to
whatever the entity already has. **Both take the angle in radians**, where the
`eulerAngles` above takes degrees — `math.rad` converts.

```lua
e.rotation.setAxisAngle(0, 1, 0, math.rad(90))    -- face 90° of yaw (Y is up)
e.rotation.setAxisAngle(1, 0, 0, math.pi / 4)     -- pitch, about X
e.rotation.setAxisAngle(0, 0, 1, math.pi / 4)     -- roll, about Z
e.rotation.setAxisAngle(1, 1, 0, math.pi)         -- any axis; length is ignored

e.rotation.rotateAxisAngle(0, 1, 0, math.rad(5))  -- turn 5° further each call
```

Calling `setAxisAngle` twice with the same angle leaves the entity where the
first call put it; calling `rotateAxisAngle` twice turns it twice as far. Both
pivot in place — the position is untouched. An axis of `(0, 0, 0)` names no
direction and raises rather than resetting the entity to face forward.

### Aiming at a point

`lookAt` turns the entity so its **forward — its local -Z, the axis
`e.transform.forward` reads back — points at a world position**. The target is
whatever you have in hand: three coordinates, one point table, or another
entity, named by id, by name, or by proxy.

```lua
e:lookAt(0, 4.5, 8)               -- three coordinates
e:lookAt({ 0, 4.5, 8 })           -- one point
e:lookAt(subject)                 -- another entity, by proxy
e:lookAt("lantern")               -- ... or by name
e:lookAt(subject.position)        -- ... or wherever it stands right now
```

Everything here is world space, so a parent under either entity moves it and
the aim still lands on the point named. A trailing **up hint** decides the roll
around the aim — which way is up in the shot — and world +Y stands in when it
is left out:

```lua
e:lookAt(target, { 0.2, 1, 0 })   -- the same aim, leaning into a dutch tilt
```

Straight up and straight down are ordinary cases. `lookAt`
returns whether the rotation was written, plus the reason it was not
(`"unresolved"`, `"no-transform"`, `"incomplete-target"`, `"incomplete-up"`,
`"degenerate"`), so
an aim at a name the scene does not carry says so rather than leaving the
entity where it was and reading back as a success.

The same call is on the transform grouping — `e.transform:lookAt(target)` —
and the orientation on its own, with no entity to write it to, is
`transform.lookRotation(eyeX, eyeY, eyeZ, atX, atY, atZ, upX?, upY?, upZ?)`,
which hands back the four quaternion components a rotation setter takes:

```lua
e.rotation = { transform.lookRotation(0, 1.35, -16, 0, 4.5, 8) }
```

## Hierarchy

Entities form a tree. Parenting keeps the child's world transform, so attaching something doesn't teleport it.

```lua
local body = entity.spawn("car")
local wheel = entity.spawn("wheel", { parent = body })   -- parented at spawn
wheel.setParent(body)                                     -- or later
wheel.getParent()                                         -- parent entity, or nil
body.getChildren()                                        -- { child, … }
body.getDescendants()                                     -- the whole subtree
wheel.unparent()                                          -- becomes a root
```

The subtree is also the unit render-layer membership is usually written in — a
prop is put on a layer as one thing, not mesh by mesh:

```lua
tools.use("renderLayer", "set", "car", "stage_props", { tree = true })
--> { { name = "car", layers = { "stage_props" }, moved = 13, skipped = 0 } }
```

`moved` and `skipped` say how far that write reached, so the reply answers
"did this cover the whole prop" without a walk over `getDescendants()`. The
render-layers guide covers membership and the camera filter that reads it.

## An entity is an id or a proxy — pick the one you want

The engine calls answer with proxies — the hierarchy reads (`getChildren`,
`getDescendants`, `getParent`) and the lookups (`find`, `findAll`, `spawn`)
alike — while id strings arrive from stored data and serialized scenes.
`entity.ids` and `entity.proxies` coerce either shape — one entity, a list, a
mix, or `nil` — into a list of the one you asked for, so consuming code states
the shape it wants instead of tracking where its input came from.

```lua
for _, child in body.getChildren() do                     -- already proxies
    child.position = { 0, 1, 0 }
end

entity.proxies(savedIds)                                  -- ids -> proxies
entity.ids(entity.findAll("wheel"))                       -- proxies -> ids
entity.ids(body)                                          -- one entity -> { id }
entity.proxies(nil)                                       -- nothing -> { }
```

Every list-taking call (`entity.batchRead`, `batchWrite`, `batchAddComponent`,
`batchProxy`, `batchDespawn`) takes ids and proxies interchangeably, so the
coercion is for your own loops rather than for feeding them.

## A proxy whose entity is gone

A proxy holds an id, not the entity. Once that entity is despawned the proxy is
still a table you can hold, and asking it for anything refuses by raising, with
a message naming the id and the member:

```
EntityProxy: entity 'ent_7f21' does not exist, so 'x' has no entity to answer
for. It was despawned, or the id names something else now.
```

`e.exists` is the one member that answers on such a proxy instead of refusing,
so a consumer that holds proxies between frames asks it before it reads:

```lua
for _, e in cached do
    if e.exists then
        e.position = { e.position.x + dx, e.position.y, e.position.z }
    end
end
```

It is the confirmation every other member is gated on, handed back instead of
enforced, so `e.exists` is true exactly when the rest of the proxy will answer,
and it never raises — a proxy over an id nothing carries reads `false` rather
than refusing. `e.transform` and `e.attribute` carry `.exists` too, on the same
rule.

`entity.exists(id)` and `entity(id)` / `entity.find(name)` are the namespace
ways to ask, and they agree with `e.exists` while the entity is there and once
it is gone. They part through the teardown span: `entity.exists` reports an
entity whose despawn is queued as already absent, while that entity is still
present and its own `onDestroying` handler still reads it — so a proxy read
inside that handler answers, and `e.exists` says so.

A transform value (`e.position`, `e.rotation`, `e.localScale` and their
siblings) is itself such a proxy: its `.x/.y/.z[/.w]` come from the entity at
the moment you read them. So a value you captured earlier and hand back later —
a task's return value, an `execute` result, anything read out of Luau — carries
that refusal into the read, and what arrives in place of `{ x, y, z }` is a
marker object:

```lua
{
    __unreadable = true,          -- always present, always true
    entity = "ent_7f21",          -- the id the proxy spoke for
    proxy  = "PositionProxy",     -- which transform value it was
    field  = "x",                 -- the first component that refused
    reason = "EntityProxy: entity 'ent_7f21' does not exist, …",
}
```

`value.__unreadable` is the test, and those five keys are the marker's whole
shape — a caller reaching for `.x` on one gets nil, which is deliberate:
components substituted with zero would read as an entity at the origin. `field`
is the first component that refused; the read stops there. Everything beside
the marker in the same payload arrives intact, so one dead leaf costs you that
leaf and nothing else.

Values going the other way are reported rather than substituted: a component's
data table carrying a dead proxy fails the call that passed it, naming the
entity — at the call where the call is synchronous, and through the promise it
handed back where it is not. A component written from a dead proxy is a value
nothing downstream can correct, so the write never happens.

## 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:

```lua
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:

```lua
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.

## There, active, visible, temporary — four independent ideas

| You want… | Use | Effect |
|---|---|---|
| To know whether the entity is still in the world at all | read `e.exists` | The only member that answers once the entity is gone; every other one refuses |
| 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 — "gone", "disabled", "invisible", and "not saved" are genuinely different states, and a disabled entity is still there to answer for itself.

## 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:

```lua
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:

```lua
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

```lua
entity.find("lantern")        -- first proxy with that name, or nil
entity.findAll("lantern")     -- every match, as proxies
entity.findAll()              -- every entity in the scene, as proxies
entity.exists(id)             -- boolean (e.exists is the same answer on the proxy)
queryEntitiesTable()          -- one cached snapshot of every entity { id, name, parentId, components }
```

Both lookups take a **glob** as well as an exact name: `*` stands for any run of
characters and `?` for any single one, matched against names.

```lua
entity.findAll("lantern_*")   -- every lantern, filtered inside the engine
entity.find("lantern_?")      -- the first single-suffix lantern
```

Reach for the glob rather than scanning `findAll()` and filtering in Luau — the
engine walks the entity set once and builds a proxy only for the matches. A glob
matches names, not ids. The argument is always a filter, so `findAll("")` asks
for entities named `""` and normally answers with none; it's `findAll()` with no
argument that means everything.

`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.
