Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scripting

Live Demo: Block Breaker with Scripts

Nightshade supports runtime scripting using Rhai, an embedded scripting language for Rust. Scripts run each frame and communicate with the engine through scope variables — reading entity transforms, input state, and time, then writing back updated positions, rotations, and commands.

Enabling Scripting

Add the scripting feature:

nightshade = { git = "...", features = ["engine", "scripting"] }

Script Component

#![allow(unused)]
fn main() {
pub struct Script {
    pub source: ScriptSource,
    pub enabled: bool,
}

pub enum ScriptSource {
    File { path: String },
    Embedded { source: String },
}
}

Scripts can be loaded from a file path (with hot-reloading on native) or embedded as a string:

#![allow(unused)]
fn main() {
let script = Script::from_source(r#"
    pos_x += dt * 5.0;
"#);

let script = Script::from_file("scripts/enemy.rhai");
}

Attaching Scripts to Entities

#![allow(unused)]
fn main() {
let entity = world.spawn_entities(
    LOCAL_TRANSFORM | GLOBAL_TRANSFORM | SCRIPT,
    1
)[0];

world.core.set_script(entity, Script::from_source(r#"
    pos_y = (time * 2.0).sin() + 1.0;
"#));
}

Scripts are disabled by default. Enable them to start execution:

#![allow(unused)]
fn main() {
if let Some(script) = world.core.get_script_mut(entity) {
    script.enabled = true;
}
}

Scope Variables

Scripts communicate with the engine entirely through variables injected into the Rhai scope. The system reads these variables after script execution to apply changes.

Transform Variables

Read and write the entity's local transform:

VariableTypeDescription
pos_x, pos_y, pos_zf64Entity position
rot_x, rot_y, rot_zf64Entity rotation (Euler angles in radians)
scale_x, scale_y, scale_zf64Entity scale

Changes are only applied if the values actually differ from the current transform (compared with epsilon tolerance).

Time Variables

VariableTypeDescription
dt / delta_timef64Frame delta time in seconds
timef64Accumulated total time since scripts started

Input Variables

VariableTypeDescription
mouse_x, mouse_yf64Current mouse position
pressed_keysArrayCurrently held key names (e.g., ["W", "SPACE"])
just_pressed_keysArrayKeys pressed this frame (not held from previous)

Key names are uppercase strings: A-Z, 0-9, SPACE, ENTER, ESCAPE, SHIFT, CTRL, ALT, TAB, BACKSPACE, UP, DOWN, LEFT, RIGHT.

Entity Access

VariableTypeDescription
entity_idi64This entity's ID (constant)
entitiesMapNamed entities with their positions and scales
entity_namesArrayList of all named entity names

Access other entities by name:

let player = entities["Player"];
let player_x = player.x;
let player_y = player.y;
let player_z = player.z;

Game State

A shared state map persists across frames and is accessible to all scripts:

state["score"] = state["score"] + 1.0;
state["game_over"] = 1.0;

State values are f64. The state map is shared across all script entities.

Spawning and Despawning

Set these variables to spawn or despawn entities:

VariableTypeDescription
do_spawn_cubeboolSpawn a cube at (spawn_cube_x/y/z)
spawn_cube_x/y/zf64Spawn position for cube
do_spawn_sphereboolSpawn a sphere at (spawn_sphere_x/y/z)
spawn_sphere_x/y/zf64Spawn position for sphere
do_despawnboolDespawn this entity
despawn_namesArrayNames of other entities to despawn

Example Scripts

Moving Object

let speed = 5.0;
pos_x += speed * dt;

if pos_x > 10.0 {
    pos_x = -10.0;
}

Keyboard Control

let speed = 8.0;

if pressed_keys.contains("W") { pos_z -= speed * dt; }
if pressed_keys.contains("S") { pos_z += speed * dt; }
if pressed_keys.contains("A") { pos_x -= speed * dt; }
if pressed_keys.contains("D") { pos_x += speed * dt; }

if just_pressed_keys.contains("SPACE") {
    do_spawn_sphere = true;
    spawn_sphere_x = pos_x;
    spawn_sphere_y = pos_y + 1.0;
    spawn_sphere_z = pos_z;
}

Follow Player

let speed = 3.0;

if "Player" in entities {
    let player = entities["Player"];
    let dx = player.x - pos_x;
    let dz = player.z - pos_z;
    let dist = (dx * dx + dz * dz).sqrt();

    if dist > 1.0 {
        pos_x += (dx / dist) * speed * dt;
        pos_z += (dz / dist) * speed * dt;
    }
}

Rotating Object

let rotation_speed = 1.0;
rot_y += rotation_speed * dt;

Bobbing Animation

let amplitude = 0.5;
let frequency = 2.0;
pos_y = 1.0 + (time * frequency).sin() * amplitude;

Scorekeeping

if !("score" in state) {
    state["score"] = 0.0;
}

if just_pressed_keys.contains("E") {
    state["score"] = state["score"] + 10.0;
}

Despawning Named Entities

if just_pressed_keys.contains("X") {
    despawn_names.push("Enemy_1");
    despawn_names.push("Enemy_2");
}

Script Runtime

The ScriptRuntime manages compilation, caching, and execution:

#![allow(unused)]
fn main() {
pub struct ScriptRuntime {
    pub engine: rhai::Engine,
    pub game_state: HashMap<String, f64>,
}
}

The engine automatically runs scripts each frame via the FrameSchedule. The scripting system is registered as system_names::RUN_SCRIPTS and executes after physics but before animation. No manual call is needed — attaching a Script component to an entity and setting enabled = true is sufficient.

Script Compilation

Scripts are compiled to AST on first execution and cached by a hash of the source code. Recompilation only occurs when the source changes. For file-based scripts, modification times are tracked and the script is automatically recompiled when the file changes (hot-reloading on native only).

Custom Functions

Register additional Rhai functions:

#![allow(unused)]
fn main() {
runtime.engine.register_fn("custom_function", |x: i64, y: i64| {
    x + y
});
}

Game State

The runtime's game_state map is injected into every script's scope as the state variable. Values persist across frames:

#![allow(unused)]
fn main() {
runtime.set_state("difficulty".to_string(), 1.0);
let score = runtime.get_state("score");
runtime.reset_game_state();
}

Hot Reloading

On native platforms, file-based scripts are automatically hot-reloaded when modified. The runtime tracks file modification times and invalidates the compiled cache when changes are detected. This allows editing scripts in an external editor while the game is running.