Skip to content

Anatomy of a script

Compiles

Real scripts have a few more parts than hello world. This one is a welcome mat: when a player steps on it, it greets them by name, but no more than once every three seconds.

Hover over a step, or tab to it, to light up the lines it describes.

welcome_mat.vscript
// welcome_mat.vscript: greets every player who steps on the mat.
//
// Place it on an entity that also has a box_trigger component. The trigger
// tells every script on its entity when something overlaps it.
// Seconds to wait before greeting again.
let cooldown_seconds: float = 3.0;
// What to say. {} is replaced by the player's name.
let greeting: string = "Welcome, {}!";
// When we last greeted someone, in milliseconds of game time.
let mut last_greeting_ms: int = -100000;
// How many greetings so far.
let mut greeted: int = 0;
// The trigger calls this when something overlaps it.
event export fn trigger_overlapped(other, unused) {
if !other {
return;
}
if !cooled_down() {
return;
}
let player: Entity = find_player_entity($component.get_entity(other));
if !player {
return;
}
let user: User = $entity.get_owning_user_from_entity(player);
$debug.print_line(greeting, [$user.get_name(user, false)], false);
greeted = greeted + 1;
last_greeting_ms = $time.get_gameplay_time_ms();
}
// True once enough time has passed since the last greeting.
fn cooled_down() -> bool {
let waited: int = $time.get_gameplay_time_ms() - last_greeting_ms;
return waited >= $time.seconds_to_ms(cooldown_seconds);
}
// The player entity that `touched` belongs to, or null if it is not a player.
fn find_player_entity(touched: Entity) -> Entity {
let player: Component = $entity.get_hierarchy_singleton("catch_player.vscript", touched);
if !player {
return null;
}
return $component.get_entity(player);
}
  1. What it is and how to use itlines 1-4

    A comment at the top saying what the script does and what it needs. This one needs a box_trigger on the same entity; that’s the component that notices players walking in.

  2. Settings: constantslines 6-10

    Lines that start with let at the top of a script are fieldsfield: A variable declared at the top of a script. Every component made from the script has its own copy.. A plain let is a constantconstant: A field declared with plain let. The script cannot change it while running, but a level can set a different value for each placed component in the editor.: the script never changes it, but whoever places the mat in a level can set their own value, such as a longer cooldown or a different greeting. The part after : is the type: float is a number with a fractional part, string is text.

  3. State: mutableslines 12-16

    let mut makes a mutablemutable: A field declared with let mut. The script's running state: it changes while the game runs. field, one the script changes as it runs. These remember things between events: when we last greeted someone, and how many greetings so far. int is a whole number.

  4. The eventlines 18-19

    The box trigger calls trigger_overlapped when something overlaps it, handing over two values. We name them other (the component that walked in) and unused (we don’t need it).

  5. Leave earlylines 20-25

    Guard clauses. ! means not: if there is no other, or the cooldown hasn’t passed, return: stop here and do nothing. Leaving early keeps the rest of the function about the normal case.

  6. Is it a player?lines 27-30

    Balls overlap triggers too. We ask our own helper function whether the thing that overlapped belongs to a player; if not, we stop.

  7. Greet themlines 32-33

    Find the useruser: A person playing, or a bot. Players, the balls they hold and the things they spawn are owned by a user. who owns that player, read their name, and print the greeting with the name filled into {}.

  8. Remember itlines 35-36

    Update the state: one more greeting, and the time it happened. = stores a new value in a mutable.

  9. A helper that answers a questionlines 39-43

    A plain fn is a helper only this script calls. -> bool says it hands back a bool (true or false). return gives the answer: has at least cooldown_seconds passed?

  10. A helper that finds somethinglines 45-52

    Takes an Entity, looks for the catch_player script anywhere in that entity’s hierarchy, and returns the player’s entity, or null (nothing) when it isn’t a player.

Almost every script you’ll read or write has the same four layers, top to bottom:

Layer What it is Written as
Parent Optional. The script this one builds on. super "path" (Inheritance)
Fields Settings and state, one copy per component. let and let mut
Events What the engine (or other scripts) call. event export fn
Helpers Your own functions, to keep events short. fn

Put fields before any function. After that, order functions however reads best; the language doesn’t care.

For programmers

Helpers are statically bound; virtual/override exist for inheritance. Parameters without a type (other, unused) take any value (var). Locals and fields are strongly typed at compile time where annotated, and $entity.get_hierarchy_singleton returns Component, which is nullable, as every engine handle is. There are no exceptions: natives signal failure by returning null/0/false, so check results.