Scripting

Full reference of the sandbox.* API: events, reading game state, commands, variables and examples ready to paste into the studio.

Scripts add behaviour to a game without touching the engine. They are plain JavaScript files, stored in the game document and run inside an isolated Web Worker, with no access to the page, the DOM or the network. They read a snapshot of the game state and answer with commands the engine applies.

Rules or scripts?

Most games only need the no-code rules. Move to scripts when you need loops, maths, randomness or logic that depends on where entities are.

Your first script

This script counts collected coins and wins the game at ten. It subscribes to two events with sandbox.on, writes a variable with sandbox.set and ends the game with sandbox.win.

coins.jsJavaScript
sandbox.on('start', () => {
  sandbox.set('coins', 0)
  sandbox.message('Collect 10 coins', 3)
})

sandbox.on('pickup', ({ role }) => {
  if (role !== 'coin') return
  const n = (sandbox.get('coins') ?? 0) + 1
  sandbox.set('coins', n)
  if (n >= 10) sandbox.win('All coins collected')
})
  1. Go to the Rules step

    The Scripts panel is the page’s fifth section. Click + New, or add one of the bundled examples.
  2. Paste the code and check it

    The Run check button (or CtrlEnter / ⌘⏎) only checks the syntax: it never runs the code.
  3. Test

    Switch to the Test step. sandbox.log() lines and errors show up in the Scripts panel console.

Where scripts live

Scripts are part of the SandboxGame document, in the optional scripts field. They are therefore hashed and published with the game: a player runs exactly the code you published.

types.tsTypeScript
interface ScriptDoc {
  id: string
  name: string
  code: string
  enabled: boolean
}

// SandboxGame
scripts?: ScriptDoc[]
idstringrequired
Unique id of the script in the document.
namestringrequired
Name shown in the panel and prefixed to every log line.
codestringrequired
The source code, at most 32,000 characters.
enabledbooleanrequired
A disabled script is kept but never compiled.

A document can hold up to 16 scripts. They are compiled and called in document order; several scripts can listen to the same event.

Events

Register a handler with sandbox.on(event, handler). Several handlers per event are allowed; they run in registration order.

TypeScript
sandbox.on(event: 'start' | 'tick' | 'touch' | 'interact' | 'pickup' | 'kill' | 'timer' | 'variable', handler: (payload) => void): void
EventHandler argumentWhen
startnoneonce, after the scripts are compiled (and again after a watchdog restart)
tickdtup to 30 times per second while the game runs; dt is the time in seconds since the previous tick
touch{ entityId, role, tag }the player touched an entity with a trigger
interact{ entityId, role, tag }the player pressed E on an entity with a trigger, or talked to an NPC
pickup{ entityId, role, tag }the player collected a coin or a key
kill{ entityId, role, tag }an enemy died, including through sandbox.hurt
timer{ name }a timer set with sandbox.timer elapsed
variable{ name, value, previous }a variable changed, whoever changed it (script, trigger, rule, game)

The tag field

In { entityId, role, tag } payloads, tag is the entity’s tag parameter, otherwise its label, otherwise an empty string. Give your entities a tag in the inspector to recognise them easily.

door.jsJavaScript
sandbox.on('interact', ({ tag }) => {
  if (tag === 'lever') sandbox.message('Something opened far away', 2)
})

Reading state

The engine sends the worker a snapshot of the world, at most 30 times per second. Everything below reads that snapshot and returns a copy: mutating what you get changes nothing in the game. To act, use commands.

sandbox.timenumber
Seconds since the game started.
sandbox.player{ id, x, y, z, yaw, hp }
The player: position, heading in degrees and health.
sandbox.entities(){ id, role, x, y, z, alive, tag, hp }[]
Every actor in the world, player included.
sandbox.find(id)Entity | null
One entity by id, or null.
sandbox.byRole(role)Entity[]
Entities of a role ('coin', 'enemy', 'npc'…).
sandbox.dist(a, b)number
Distance between two things that have x, y, z.
sandbox.get(name)unknown
A variable’s value, undefined when unset.
sandbox.hud{ status, score, time, message }
The HUD state: game status, score, time, current message.

Entity roles

The role field is the document’s EntityRole. The snapshot lists runtime actors, so any role present in the game can show up: player, prop, obstacle, coin, checkpoint, finish, enemy, hazard, boost, spawn, text, door, key, platform, trigger, npc, light, spawner, sound, flag, base, turret, waypoint. See Entity roles.

nearest.jsJavaScript
sandbox.on('tick', () => {
  const p = sandbox.player
  const near = sandbox.byRole('enemy').filter((e) => e.alive && sandbox.dist(p, e) < 3)
  if (near.length) sandbox.message('Watch out!', 0.5)
})

Commands

Commands are queued during the handler and applied by the engine, in order, on the next frame. They return nothing. Arguments are coerced (numbers, strings) and validated engine side: an unknown id is ignored without an error.

sandbox.set

TypeScript
sandbox.set(name: string, value: unknown): void

Writes a variable (any JSON-serialisable value). Visible to sandbox.get at once in the same script. See Variables.

sandbox.message

TypeScript
sandbox.message(text: string, seconds = 2.5): void

Shows a HUD message for the given duration.

sandbox.score

TypeScript
sandbox.score(delta: number): void

Adds (or removes, with a negative value) points to the score.

sandbox.teleport

TypeScript
sandbox.teleport(x: number, y: number, z: number, entityId?: string): void

Moves the player, or the given entity, and resets its velocity. The camera follows a teleported player.

sandbox.remove / sandbox.show

TypeScript
sandbox.remove(entityId: string): void
sandbox.show(entityId: string): void

remove hides an entity (never the player); it can be shown again with show, which also reveals entities hidden at start.

sandbox.spawn

TypeScript
sandbox.spawn(assetId: string, role: EntityRole, x: number, y: number, z: number, params?: EntityParams): void

Creates an entity from an asset of the document. params is an EntityParams (hp, behavior, speed, tag…). Without params, an enemy gets { behavior: 'chase', speed: 2.4, hp: 1 }. The entity rests on the ground when y is below the terrain; prop and obstacle roles are solid.

Finding an asset id

Asset ids are the document’s (doc.assets[i].id). The usual trick: copy an existing entity through sandbox.byRole('enemy')[0], or store the id in a variable (sandbox.set('enemyAsset', '…')) then sandbox.spawn(sandbox.get('enemyAsset'), …). At most 20 spawns per frame and 400 actors in total; past that, spawns are ignored.

sandbox.hurt / sandbox.heal

TypeScript
sandbox.hurt(entityId: string, amount = 1): void
sandbox.heal(amount = 1): void

hurt on the player removes health; on another entity it removes hp, and the entity dies at 0 (the kill event then fires). heal heals the player.

sandbox.win / sandbox.lose

TypeScript
sandbox.win(text?: string): void   // default "You made it"
sandbox.lose(text?: string): void  // default "Game over"

Ends the game, won or lost, with optional text.

sandbox.move / sandbox.look

TypeScript
sandbox.move(entityId: string, dx: number, dy: number, dz: number): void
sandbox.look(entityId: string, yawDeg: number): void

move offsets an entity by (dx, dy, dz); look sets its heading in degrees.

sandbox.timer / sandbox.clearTimer

TypeScript
sandbox.timer(name: string, seconds: number, repeat = false): void
sandbox.clearTimer(name: string): void

Fires the timer event with { name } after seconds (0.05 s minimum), once or repeating. Timers run inside the worker, with no round trip to the engine. Setting a timer with the same name replaces it; clearTimer cancels it. Each script has its own timers.

sandbox.log

TypeScript
sandbox.log(...args: unknown[]): void

Prints to the studio console, prefixed with the script name. Objects are JSON-stringified. At most 20 lines per handler.

Variables

Variables belong to the engine. Scripts, triggers, rules and game systems can all read and write them, and the HUD can display them (Variables and HUD). The variable event fires for every change seen between two ticks.

  • A set is visible to get immediately in the same script, even though the engine only stores it a frame later: a counter incremented on two consecutive events counts both.
  • If the engine writes a different value in between, the engine’s value wins.
  • Variables survive a watchdog restart, unlike JavaScript variables in your closures: keep the state that matters in sandbox.set.

Examples

The first three are also one click away in the studio Scripts panel.

coins.jsJavaScript
// Counts coin pickups and wins the game at 10.
sandbox.on('start', () => {
  sandbox.set('coins', 0)
  sandbox.message('Collect 10 coins', 3)
})

sandbox.on('pickup', ({ role }) => {
  if (role !== 'coin') return
  const n = (sandbox.get('coins') ?? 0) + 1
  sandbox.set('coins', n)
  sandbox.message(`${n} / 10`, 1)
  if (n >= 10) sandbox.win('All coins collected')
})

The Scripts panel

On the Rules step, the panel lists the document’s scripts (16 at most), with a checkbox to enable or disable each one, a delete button and the examples to add. The editor indents with Tab (two spaces) and checks the syntax with CtrlEnter. The console keeps the last 50 lines: your sandbox.log() output, runtime errors and watchdog messages.

See also