Appearance
Scripting basics
Scripts attach to entities and run on the authoritative server. Each script is a class that extends Behaviour (like a Unity MonoBehaviour): instance fields hold per-entity state, and you override lifecycle methods (onStart, onUpdate, …). Inside a method, this.entity, this.game, this.players, this.math, … give you the engine API.
The Script Editor autocompletes the whole API as you type, and the Script API reference lists every global, method, and hook.
Your first script
In the Project panel choose New → Script and you get a class to fill in:
js
class Spinner extends Behaviour {
speed = 90 // public field — editable per-entity in the Inspector
onUpdate(dt) {
this.entity.rotate([0, this.speed * dt, 0]) // speed° per second
}
}Attach it to an entity (add a Scripts component pointing at this script) and press Play. Each entity that uses the script gets its own instance — its own speed, its own state.
Lifecycle methods
Override the ones you need:
onAwake()— once, before anyonStart(initialize internal state here)onStart()— once, after every entity'sonAwake(safe to reference other scripts viagetScript)onUpdate(dt)— once per rendered frame;dtvaries with the framerateonFixedUpdate(dt)— once per physics step (20 Hz), just before it runs;dtis alwaysTime.fixedDeltaTimeonCollision(other)— when this entity's rigid body contacts another (needs colliders; at least one dynamic)onPlayerEnter(player)/onPlayerLeave(player)— trigger zonesonPlayerRespawn(player)— fires on every script when a player dies + respawns; reset NPCs/obstacles to their start here
See the full list of hooks.
Update or FixedUpdate?
This is the same split Unity makes, and it exists for the same reason:
onUpdate(dt) | onFixedUpdate(dt) | |
|---|---|---|
| runs | once per rendered frame | once per physics step (20 Hz) |
dt | varies with the framerate | always Time.fixedDeltaTime |
| use it for | cameras, input, UI, animation | forces, impulses, velocity changes |
Rendering and physics run at different rates: your display might be 60 Hz or 144 Hz, but physics always steps at 20 Hz. Forces you apply pile up until the next step consumes them — so a force applied from onUpdate is multiplied by however many frames landed in that step (about 3× at 60 fps, 7× at 144 fps) and behaves differently on every machine. onFixedUpdate runs exactly once per step, so what you ask for is what gets simulated, identically in the editor, a standalone build and on the multiplayer server.
js
class Hover extends Behaviour {
lift = 900
onFixedUpdate() { // physics — one call, one step
physics.applyForce(this.entity.id, [0, this.lift, 0])
}
onUpdate() { // visuals — as smooth as the display
this.camera.lookAt(this.entity.position)
}
}If you write Unity-style, Update() and FixedUpdate() map to these respectively.
TIP
A rule of thumb: if you're calling anything on physics, it belongs in onFixedUpdate. The reverse holds for edge input: input.getKeyDown / getKeyUp report a single frame's press or release, and a fixed step doesn't run once per frame — a fast frame may run no step (the press is missed) and a slow frame may run two (it's seen twice). Read held state (input.getKey) in onFixedUpdate; handle presses in onUpdate. Unity has exactly the same caveat.
TIP
Read scene state in onStart, not in a field initializer — fields are set while the entity is still being wired up, so this.entity isn't available yet at that point.
Instance state
Use instance fields (this.health) for per-entity state — they persist across ticks and every entity has its own copy:
js
class Enemy extends Behaviour {
health = 100
onCollision(other) {
if (other.tag === 'Bullet') {
this.health -= 25
if (this.health <= 0) this.game.destroyEntity(this.entity.id)
}
}
}Inspector fields
Public fields with a simple default — a number, string, boolean, or [x, y, z] vector — appear in the Inspector under the script, so you can tweak them per entity without editing code. Each entity keeps its own values; the code default is used until you override it.
js
class Patrol extends Behaviour {
speed = 2 // number input
loop = true // checkbox
home = [0, 0, 0] // x / y / z inputs
}Fields whose name starts with _ stay private (not shown in the Inspector).
Field attributes
Annotate fields with Unity-style comments to control how they appear:
js
class Patrol extends Behaviour {
// [Header("Movement")]
// [Range(0, 10)]
speed = 2 // becomes a slider 0–10
// [Tooltip("Loops back to the start")]
loop = true
}// [Header("…")]adds a section label above the field.// [Range(min, max)]turns a number field into a slider.// [Tooltip("…")](or a trailing// comment) shows hover help.
Entity references
Mark a field // [Entity] with a null default to assign another entity to it from a dropdown in the Inspector — like dragging a GameObject into a slot in Unity:
js
class Turret extends Behaviour {
// [Entity]
target = null
onUpdate() {
if (!this.target) return
const t = this.game.getEntity(this.target) // live handle — read AND write
if (t) t.position = [t.position[0], t.position[1] + 0.1, t.position[2]]
this.getScript(this.target, 'Health')?.damage(1) // or call its scripts
}
}this.target holds the chosen entity's id. Turn it into a live handle with this.game.getEntity(this.target) (read and write its transform/components), use this.getScript(this.target, '…') to call its scripts, or this.game.findEntityById(this.target) for a read-only snapshot.
Reading & writing components
Scripts can read and edit the components on an entity — its meshRenderer, rigidBody, lookAt, navMeshAgent, and so on. These methods are available as this.* on a Behaviour, and the game version takes an entityId to reach other entities.
js
this.getComponent('meshRenderer') // read a snapshot (copy) — or null
this.setComponent('meshRenderer', { color: '#ff0000' }) // merge-patch existing fields
this.addComponent('lookAt', { target: id }) // add (or overwrite) a whole component
this.removeComponent('lookAt') // drop it (you can't remove 'transform')
this.hasComponent('rigidBody') // booleangetComponent returns a live write-back handle. Reading gives the component's current values; writing a field patches the real component and broadcasts the change, exactly as setComponent would:
js
this.getComponent('pointLight').intensity = 5 // the light actually dimsFields that are baked into the physics body when it's built — collider.isTrigger, rigidBody.mass, rigidBody.gravityScale — throw rather than accept a write that couldn't take effect. The error names the field and what to do instead. rigidBody also carries live simulation members (velocity, AddForce, friction…); see the RigidBody handle.
You can also change a component wholesale:
setComponent(type, patch)is the ergonomic editor — it merges your patch onto the component's current fields and leaves the rest alone (it creates the component if it isn't there):js// turn a light red and dim it — its other fields are untouched this.setComponent('pointLight', { color: '#ff3344', intensity: 0.5 })addComponent(type, data)sets the whole component, overwriting any existing one. Use it when you want to replace everything — including the build-time fields a live write refuses:jsthis.addComponent('rigidBody', { type: 'dynamic', mass: 2, linearDamping: 0.1 })
Edits reach the live entity immediately, so engine systems that read components — lookAt, followTarget, navMeshAgent, the renderer — pick them up the same tick. See Transform constraints for driving those from a script.
A few patterns
Move forward at a constant speed (frame-rate independent):
js
class Mover extends Behaviour {
speed = 4
onUpdate() {
// entity.forward is a direction vector; time.deltaTime keeps it frame-rate independent
this.entity.translate(this.math.mul(this.entity.forward, this.speed * this.time.deltaTime))
}
}Spawn a prefab on a timer (coroutine):
js
class CoinSpawner extends Behaviour {
onStart() { this.startCoroutine(this.loop()) }
*loop() {
while (true) {
this.game.instantiate('Coin', [this.math.randomRange(-5, 5), 1, 0])
yield wait(2)
}
}
}Chase the nearest player:
js
class Chaser extends Behaviour {
speed = 3
onUpdate(dt) {
const player = this.players.getAll()[0]
if (!player) return
const dir = this.math.normalize(this.math.sub(player.position, this.entity.position))
this.entity.translate(this.math.mul(dir, this.speed * dt))
}
}Reward a player who steps on a pad — add a Trigger component (box or sphere) to the entity plus this script; onPlayerEnter/onPlayerLeave fire when a player enters/leaves that zone:
js
class CoinPad extends Behaviour {
reward = 1
onPlayerEnter(player) {
this.economy.addCoins(player.id, this.reward)
this.ui.showMessage('+' + this.reward + ' coin!', player.id)
}
}Coroutines
For sequences over time — spawn waves, timed effects, step-by-step logic — use a coroutine: a generator method you start with this.startCoroutine(...) and pause with yield wait(seconds) or yield waitFrames(n).
js
class WaveSpawner extends Behaviour {
onStart() {
this.startCoroutine(this.spawnWaves())
}
*spawnWaves() {
for (let wave = 1; wave <= 3; wave++) {
this.ui.showMessage('Wave ' + wave)
for (let i = 0; i < wave * 3; i++) {
this.game.spawnEntity('Enemy', 'box', [i, 1, 0])
yield wait(0.5) // half a second between enemies
}
yield wait(3) // breather between waves
}
}
}Call the generator method (this.spawnWaves()) when you start it — just like Unity's StartCoroutine(SpawnWaves()). startCoroutine returns an id for stopCoroutine(id), or call this.stopAllCoroutines().
Function-style scripts (still supported)
The older style still works — define top-level onStart() / onUpdate(dt) functions and use the globals directly (entity, game, …). Existing scripts keep running unchanged; new scripts default to the class style above.
js
function onUpdate(dt) {
entity.rotate([0, 90 * dt, 0])
}Everything available to scripts is in the Script API reference.
Next steps
- Scripting your own player — turn off the built-in player and drive your own character + camera (the
camera,cursor,characterController, andinput.getMouseDeltaAPIs). - Transform constraints — make entities follow / face a target with no script (
lookAt,followTarget). - Exporting a standalone build — ship your map as an offline, single-player game.