Events and handlers
Subscribe script methods to events with @on, filter them by entity kind, throttle them, and understand handler order and faults.
On this page
Scripts react to events. A method decorated with @on(event) is a handler: it runs every time that event reaches the script. This page covers the decorator, the built-in tick event, events your game sends, and what happens when a handler fails.
The @on decorator#
import { World, on } from "neo";
export default class Doors extends World {
@on("interact", { tag: "Door" })
open(player, door) {
door.open();
}
@on("tick", { every: 2 })
drop() {
// at most once every 2 simulation seconds
}
}from neo import World, on
class Doors(World):
@on("interact", tag="Door")
def open(self, player, door):
door.open()
@on("tick", every=2)
def drop(self):
# at most once every 2 simulation seconds
pass
default = Doors| Option | Meaning |
|---|---|
event | The event name, any string. "tick" is the built-in one. |
tag | A kind name. The handler runs only when the event's second argument is an entity of that kind your script can see. Events shaped like (player, target) use this to pick the target. |
every | Seconds of simulation time. The handler runs at most once per every seconds, counted from the first time it ran. It works on any event, not only tick. |
Rules for decorators:
@ongoes on instance methods only. Static methods, getters and setters can't be handlers.- A method can carry more than one
@on, so one method can handle several events. - Handlers may be
async. See Time, sleep and async.
The tick event#
@on("tick") runs once per simulation tick. Its one argument is dt, the simulation time since the previous tick in milliseconds:
@on("tick")
move(dt) {
this.state.lap += (dt / 1000) * 0.1; // 0.1 laps per second
}@on("tick")
def move(self, dt):
self.state.lap += (dt / 1000) * 0.1 # 0.1 laps per secondThe game runs scripts on its fixed timestep, so dt depends on the game's fixed rate, not on the frame rate. Use this.time when you need the absolute time.
Events from the game#
A game sends its own events from Rust by writing a ScriptEvent before the scripts run:
events.write(ScriptEvent::new("touch").entity(player).entity(platform));Each .entity(e) or .value(v) becomes one handler argument, in order. So this event reaches:
@on("touch", { tag: "Platform" })
touched(player, platform) { ... }@on("touch", tag="Platform")
def touched(self, player, platform): ...- An entity argument arrives as an entity object (see The World API). An entity the game hasn't exposed to scripts at all arrives as
null. - Value arguments arrive as plain data: numbers, strings, lists, and maps (dicts in Python).
- Events are delivered the next time the scripts run, before that tick's
tickhandlers. A system ordered.before(ScriptSystems)inFixedUpdatedelivers its events in the same fixed tick.
Which events a game sends is up to the game. If your game uses the gameplay kit, see Gameplay kit for what it gives scripts.
Events from scripts#
this.emit(event, payload) sends an event out. The game reads it in Rust as a ScriptEmitted message with three fields: script (the script's name), event and payload. Systems ordered after the scripts see it in the same tick. Emitted events don't reach other scripts' handlers.
Handler order#
Everything about order is fixed, so every run is the same:
- Scripts run one after another, in the order they were loaded. Changes a script makes are applied to the game before the next script runs, so later scripts see them.
- For each script, the tick's game events are delivered first, in the order the game wrote them.
- Then the script's tick runs: timers that are due resolve first (see Time, sleep and async), then the
tickhandlers. - When several handlers match one event, they run in the order the methods appear in the class.
Faults#
A fault is a handler that stopped because of an error. Causes include:
- an uncaught exception (including a denied field, method or spawn);
- a rejected promise in an
asynchandler; - running out of fuel or memory (see Limits and determinism);
- a Rust method that returned an error, or a change the game couldn't apply (for example, writing to an entity that is gone).
When a handler faults:
- the rest of that handler doesn't run, but changes it made before the error stay made;
- other handlers and later ticks run normally: one fault doesn't stop the script;
- the script's fault count goes up, and the last fault's text (the handler name and the reason) is shown in the Scripts panel and by
script.list.
You can catch ordinary errors yourself with try/catch (or try/except), which is the right choice when a denied action is expected:
try { door.open = true; } catch (e) { this.state.denied = [...this.state.denied, door.id]; }try:
door.open = True
except PermissionError:
self.state.denied = [*self.state.denied, door.id]Running out of fuel or memory can't be caught.