---
title: "Component (asset type)"
description: "A component is a reusable behavior + data unit attached to an entity. The component file declares public fields, methods, and lifecycle callbacks; the engine instantiates one per attachment and…"
section: "Types"
slug: "types-component"
canonical: "https://origozero.ai/docs/types-component"
updated: "2026-09-06T10:26:33.829296967+00:00"
tags: ["asset-type", "reference"]
---

# Component (asset type)

## When to use one

- You need per-entity state that survives across frames.
- You need per-entity behavior driven by engine lifecycle hooks
  (`start`, `update`, `destroy`).
- You want to expose configurable fields to designers / `entity.spawn`
  callers / scenes without code changes.

If you need a global service that outlives any entity, use a `.service`
instead. If you need a stateless pure-function library, use a `.module`.

## Where it lives

- Source: `/zero/source/.../<Name>.component/`
- Identity: `<Name>` (the `.component` suffix strips from the
  identity, but the folder retains the suffix on disk).
- Folder shape:
  - `init.luau` (or `init.lua`) — declares the component. **Required.**
  - `README.md` — instance-level documentation. **Required.**
  - `.metadata` — agent-editable tags + free-form fields. **Required.**

The structural contract is enforced by `asset.validate` against this
type's `type.yaml`.

## How to create one

Always start with the scaffold — it guarantees the folder shape
matches `type.yaml` from the moment it's on disk:

```luau
asset.create("component", "<Name>")
-- Creates: /zero/source/<Name>.component/
--   init.luau   (canonical declare + public block + hooks stubbed)
--   README.md   (instance README template)
```

After scaffolding, edit `init.luau` to fill in the actual fields and
behaviour. The component registers as soon as the file is written —
hot-reload is automatic; no engine restart required.

## How it operates

1. **Registration.** Writing `init.luau` into a `.component/` folder
   indexes the asset, mints a guid (in the `.meta` sidecar), and
   surfaces the type to the engine's component registry. Same-frame
   visible to `entity.component.add`.
2. **Attachment.** `entity(id).component.add(name, data)` (or the
   batch form `tools.use("entityOps", "addComponents", targets,
   { [name] = data })`) attaches an instance.
   The `data` table's fields override the component's declared
   defaults; omitted fields take the declared default. The `data`
   values are copied into `public.*` BEFORE `awake()` runs.
3. **Lifecycle.** The hooks run in this order:
   - `awake()` — same frame as attach, immediately after the data
     copy. Initialise state, set up native bridges, register with
     other systems. Other components on the same entity may not have
     awoken yet.
   - `start()` — the next frame. Every component on the entity has
     completed `awake()` by then, so cross-component setup is safe
     here.
   - `update(dt)` — same frame as `start()`, then every frame after,
     for as long as the gameplay clock is running. `fixedUpdate(dt)`
     runs beside it on the fixed timestep; `editorUpdate(dt)` runs
     while the clock is held. Which of them ticks is decided by
     `engine.mode` AND `engine.paused` together — see **Per-frame
     hooks and the two axes** below, and `man modes`.
   - `onDestroy()` — when the component is removed via
     `component.remove` or when the owning entity is despawned.
     Release resources, drop native ECS components.

   For the full list (`onEnable`, `onDisable`, `fixedUpdate`,
   `editorUpdate`, `onPropertyChanged`, `onAssetReload`,
   `onModuleReload`,
   `onBeforeSave`, …) and the timing details, **read `man components`
   — that is the canonical lifecycle reference. Generic component
   patterns (declare blocks, public fields, sync, ECS bindings,
   typed methods) all live there.**
4. **Public surface.** Fields declared in `public` are inspectable,
   editable in the inspector, and persist with the entity. Methods are
   declared on that same table with a colon —
   `typed function public:method(args)` — and every one of them takes
   the component ref as `self`, so a caller invokes it with a colon
   too: `entity(id).component.get(name):method(args)`. That is the one
   calling convention a component ref answers to: the reflection
   methods `ref:fields()` / `ref:getField(name)` /
   `ref:setField(name, value)` take the same spelling. A function the
   chunk declares at file scope (`function helper()`) is the
   component's own, reachable from its hooks under its bare name;
   `public` is what the ref carries.
5. **Events.** An `events` block declares typed moments the component
   fires from inside — `events = { onHit = Event({ dmg =
   Field.number(0, NoSync) }) }`. The owner fires with
   `events.onHit:fire({ dmg = 10 })`; outside code subscribes through
   the proxy's subscribe-only facade,
   `entity(id).component.get(name).events.onHit:connect(fn)`
   (`:once` / `:wait` too). Pass `Sync` as the event's second argument
   to replicate a fire to other peers. Events let one component react
   to another's moments without polling. Full reference: **`man
   components`**, Events section.
6. **Hot reload.** Editing `init.luau` reloads the component
   definition; live instances are reconciled with the new declaration
   in-place, and each crosses one lifecycle boundary: `onDestroy()`
   ends the running life, then `awake()` and `start()` construct the
   one that replaces it, and `update()` resumes on the next frame.
   `public` and `private` values carry across; module-level `local`s
   belong to the chunk, so the new life starts with them at their
   initial values and its `awake`/`start` are what fill them in.
   Whatever the old life handed elsewhere — a subscription, a shared
   registration, a spawned entity — is released in `onDestroy`, the
   call that pairs with the construction about to run. An edit that
   leaves the source byte-identical crosses no boundary at all.

## Discovery

- `asset.list("component")` — every registered component (engine +
  world + libraries).
- `asset.inspect("<name>")` — identity, scope, public fields,
  lifecycle methods, source path, and this type README. `detail`
  carries the component's declared `fields`, `methods`, and `events`,
  parsed from its source — `tools.use("assets","describe", "<name>")`
  renders it as markdown, so a component's events are visible without
  opening its source.
- `cat /zero/source/<Name>.component` — the raw filesystem view (a
  different projection than `asset.inspect`).
- `entity(id).component.list()` — components attached to an entity.

## Per-frame hooks and the two axes

`engine.mode` (`"edit"` / `"play"`) and `engine.paused` (the gameplay
clock) are independent, and the per-frame hooks read them separately:

- `update(dt)` / `fixedUpdate(dt)` tick while **the gameplay clock is
  running** — whichever mode the engine is in.
- `editorUpdate(dt)` ticks while **the clock is held** — because the
  mode is `edit`, or because play is paused — and also alongside
  `update` whenever edit is running.

Which gives four states, and this table is the contract:

| `engine.mode` | `engine.paused` | hooks that tick |
|---|---|---|
| `edit` | `true` | `editorUpdate` |
| `edit` | `false` | `update`, `fixedUpdate`, `editorUpdate` |
| `play` | `false` | `update`, `fixedUpdate` |
| `play` | `true` | `editorUpdate` |

Entering edit sets `paused = true` and entering play sets it back to
`false`, so a session that never writes `engine.paused` only ever sees
rows 1 and 3 — which is where the shorthand "`update` is the play tick,
`editorUpdate` is the edit tick" comes from. It is the two default rows
of a four-row table, not the rule.

**Row 2 is the one that surprises.** With `engine.paused = false` in
edit — set by hand, or by a tool that advances the gameplay clock to
sample it, such as the `capture` filmstrip — `update` and `editorUpdate`
both fire, once each, every frame. A component that declares both and
routes them to one body therefore runs that body **twice per frame**:

```luau
local function drive(dt) ... end
function update(dt)       drive(dt) end
function editorUpdate(dt) drive(dt) end   -- twice per frame in row 2
```

Nothing reports the doubling; it reads as an animation running at twice
its intended speed. To get exactly one tick per frame in every row, ask
which axis you meant and answer it in the body:

```luau
function update(dt)       drive(dt) end
function editorUpdate(dt) if engine.paused then drive(dt) end end
```

A scene `entrypoint.luau` declaring the same two hook names is dispatched
by the scene loader, not by this table: there `update` needs play AND
running, `editorUpdate` needs edit, and play-paused ticks neither. Read
`man scenes` before carrying a component's rule to an entrypoint.

## Authoring conventions

- Annotate public fields with `--!desc` so the inspector + LSP show
  documentation alongside the field name.
- Declare lifecycle hooks (`start`, `update`, `destroy`) explicitly
  rather than relying on engine defaults — readers can see at a glance
  what runs when.
- Keep `update(dt)` cheap — it runs every frame per instance. Move
  heavy work to `start`, to a service, or behind a job (`man jobs`).
- For multi-entity batching, prefer
  `tools.use("entityOps", "addComponents", targets, components)` over
  a loop of `entity(id).component.add` — it takes many targets and
  many components in one call.

## Common pitfalls

- **Naming.** The component identity is the folder stem with
  `.component` stripped (e.g. `<Spinner>.component/` → identity
  `Spinner`). Don't repeat the suffix in the identity passed to
  `component.add`.
- **`init.lua` vs `init.luau`.** Either is accepted (it's a
  `one_of_group: "entrypoint"`), but `.luau` is canonical. Mixing both
  in one folder is invalid.
- **`update` in edit mode.** It runs whenever the gameplay clock is
  running, and entering edit stops that clock — so `update` is idle in
  a freshly-entered edit session and ticks again the moment anything
  sets `engine.paused = false`. Use `tools.use("wld", "play")` to test
  update logic under the mode it ships in; `tools.use("wld", "edit")`
  to return. See **Per-frame hooks and the two axes** above.
- **Renaming.** Renaming the folder changes the identity. Update
  every `component.add(<oldname>, ...)` call and the scene JSON
  entries that reference it.

## Related types

- `.module` — pure-function library, no per-entity state.
- `.service` — globally-scoped behavior, no per-entity state.
- `.preset` — a captured component configuration, replayable into
  `component.add` without re-typing fields.
- `.bundle` — entity hierarchies that may carry component attachments
  as part of the bundle.
