Scripting
How scripts work on NEO, where they live in a project, and what a script looks like in JavaScript and Python.
On this page
Scripts add behaviour to a NEO game without rebuilding it: moving platforms, puzzles, timers, spawning objects, reacting to players. The game itself is built in Rust; scripts run on top of it and can only touch what the game chooses to expose.
What a script is#
Every script on NEO runs on one script VM. You can write scripts in JavaScript or Python (NEO's Python is called NeoPy). Both languages compile to the same engine, so they share the same rules for time, randomness, limits and permissions.
A few things are true of every script:
- One language per game. A game's
neo.tomlpicksjsorpy, and all of that game's scripts and modules use it. - Deterministic. The same scripts, inputs and world seed produce the same result on every run and every device. Time is simulation time, randomness comes from a world-seeded stream, and there is no wall clock.
- Sandboxed. A script can't read files, open network connections, use
evalor change built-in objects. One script can't reach another script's variables or state; scripts only meet through the world. - Nothing is reachable unless the game exposes it. A script only sees the entity kinds, fields and methods that the game declares in Rust and grants in
neo.toml. Anything else doesn't exist for the script.
Where scripts live#
Script files go in your project's assets/scripts folder (.js or .py). The [script] section of neo.toml names the language, lists the scripts the game runs, sets their budgets and says what they may touch:
[script]
language = "js"
scripts = ["assets/scripts/platform.js"]
[script.expose.Platform]
fields = { position = "get set", speed = "get" }
methods = ["boost"]A script's name is its file name without the extension, so assets/scripts/platform.js runs as platform. Files that share code with your scripts are modules: they live in the same folder and export functions instead of a world class.
The full list of keys is in The [script] manifest.
The shape of a script#
A script exports one class that extends World. Methods marked with @on(...) run when an event happens, and state holds the values the script keeps between ticks:
import { World, on } from "neo";
export default class Counter extends World {
state = { ticks: 0 };
@on("tick")
count(dt) {
this.state.ticks += 1;
}
}from neo import World, on
class Counter(World):
state = {"ticks": 0}
@on("tick")
def count(self, dt):
self.state.ticks += 1
default = CounterWorldgives the script its view of the game:state,emit,query,spawn,rngandtime. See The World API.@on("tick")runs the method every simulation tick. Games send their own events too, such as"touch"or"interact". See Events and handlers.statesurvives hot reloads and is part of the script's state hash. See Script state.
Running and editing scripts#
The game loads the scripts listed in neo.toml when it starts. In the Studio, the Scripts panel shows every running script with its fuel, faults and state hash, and saving a script hot-reloads it while the game runs. See The Scripts panel. The same controls are available as commands for tools and agents: see Script commands.
In this section#
- Your first script: write, list and run a script.
- The World API: what
this(orself) gives you. - Events and handlers:
@on, tag filters, throttling and faults. - Script state:
this.state, hot reload and naming. - Time, sleep and async: the simulation clock and
await sleep. - Exposure and permissions: what scripts may touch, and who may script.
- Modules: sharing code between scripts.
- Python scripts: what NeoPy includes.
- JavaScript and Python differences: writing scripts that behave the same in both.
- Limits and determinism: fuel, memory, calls and recursion.
- Paired examples: tested scripts in both languages.