Skip to content

Latest commit

Β 

History

History
193 lines (152 loc) Β· 8.09 KB

File metadata and controls

193 lines (152 loc) Β· 8.09 KB

Scripting Overview

Page Map

Header Link
Purpose Purpose
Canonical Script Layout Canonical Script Layout
Mental Model Mental Model
Scripting Group Scripting Group
Use Cases Use Cases
Example Example
Reference Reference

Purpose

Use this area for all script-authored game logic.

This includes state, lifecycle hooks, custom methods, runtime node access, cross-script calls, input, resources, and Variant conversion.

The book explains why Perro uses these shapes.

The docs give exact macro/API paths and edge behavior.

Canonical Script Layout

Start each attachable gameplay script with one state root, then put engine callbacks in lifecycle! and behavior methods in methods!:

use perro_api::prelude::*;

#[State]
struct GameState {
    #[default = 100]
    pub health: i32,
    velocity: Vector2,
}

lifecycle!({
    fn on_update(&self, ctx: &mut ScriptContext<'_, API>) {
        self.internal_method(ctx);
    }
});

methods!({
    fn internal_method(&self, ctx: &mut ScriptContext<'_, API>) {
        with_state_mut!(ctx.run, GameState, ctx.id, |state| {
            state.velocity.x = 0.0;
        });
    }

    pub fn externally_callable_method(
        &self,
        ctx: &mut ScriptContext<'_, API>,
        amount: i32,
    ) {
        with_state_mut!(ctx.run, GameState, ctx.id, |state| {
            state.health += amount;
        });
    }
});

GameState is a role name, not a required type name. Rename it to the owner (PlayerState, DoorState, and so on). Keep a large state root short by grouping cohesive data in nested structs that derive Variant when a dynamic boundary needs them. Do not hand-write impl GameState for script behavior; the macros provide the generated script entry points.

Keep helpers private when only this script calls them. Mark a method pub only when another script, a signal, an animation event, or call_method! must dispatch to it. Keep state fields private unless scene injection or dynamic access needs pub.

Author fixed node trees and reusable composition in .scn files. Follow the scene templates and scene docs, then attach the script from the scene. Use runtime node creation only for generated leaf data, debug/tooling nodes, and other transient objects with no reusable topology. Load authored .scn prefabs for projectiles, waves, enemies, and other gameplay objects; see runtime spawning.

Mental Model

One script instance has one owner: the node in ctx.id.

The node stores scene data. #[State] stores script-owned per-instance data. Lifecycle hooks choose when work runs. Methods target one receiver. Signals announce an event without choosing every receiver. Queries discover a set that changes at runtime. Variant crosses boundaries where the concrete Rust type is not known to the caller.

Keep those roles separate. A fixed camera belongs in a scene-injected NodeID; it is not a query. Typed health belongs behind with_state!; it is not a string lookup. A coin-collected event belongs in a signal when several independent systems may react; it is not a chain of hard-coded calls.

Scripting Group

Task Page
Follow script authoring standards Script Authoring Guide
See scripts work together Script Teamwork Examples
Write first script Project Script Modules
Store per-node data Script State
Run engine callbacks Script Lifecycle
Add callable methods Script Methods
Read/mutate nodes at runtime Runtime Nodes Module
Query nodes Query System
Call self/cross-script methods Scripts Module
Convert dynamic values Variant
Read input Input API
Load/use resources Resource API
Run CPU work in parallel Parallel Jobs

Use Cases

Situation Choose Why Tradeoff
Controller owns health, velocity, or cooldown data #[State] + typed state access Value lives with one script instance and keeps its Rust type Other script types need a shared type or dynamic boundary
Switch knows one door scene-injected NodeID + call_method! Dependency and receiver stay explicit Caller depends on method name and return schema
Coin must notify HUD, audio, and achievements signal Producer stays independent from current listeners Connection lifetime and payload schema need ownership
Manager needs every currently spawned enemy query Result reflects runtime membership Query costs more and gives weaker guarantees than a fixed ref
Tool knows a member name but not its Rust state type get_var! / set_var! Variant supports runtime-selected members Decode may fail; typed access is safer and cheaper
Pathfinding or bulk scoring costs too much in one frame job CPU work leaves the frame callback Inputs/results must cross a task boundary and cannot borrow runtime state

Choose a context by role: ctx.run for runtime state, nodes, scenes, scripts, signals, time, and window calls; ctx.res for resources and data; ctx.ipt for input.

Example

For fixed dependencies, store a NodeID in state and inject it from the scene. Use a query only when the set is dynamic:

lifecycle!({
    fn on_update(&self, ctx: &mut ScriptContext<'_, API>) {
        if action_pressed!(ctx.ipt, "interact") {
            for door in query!(ctx.run, all(tag["door"])) {
                let ret = call_method!(ctx.run, door, method!("toggle"), params![]);
                let opened = ret.parse::<bool>().unwrap_or(false);
                log_info!("door {:?} open {}", door, opened);
            }
        }
    }
});

toggle must be a pub fn in each door script's methods! block β€” call_method! only generates dispatch glue for pub methods (method visibility).

Reference

Scripting Overview

Perro scripts are authored in Rust and compiled into script modules. Perro manages most glue code for you, so scripting stays close to normal Rust instead of turning into registration boilerplate.

Core pieces:

  • #[State] data struct
  • lifecycle! for engine entry points
  • methods! for callable behavior methods
  • bare Rust modules for shared code (res/**.rs with no script behavior)
  • script contexts (RuntimeWindow, ResourceWindow, InputWindow)

Borrow rule:

  • ctx.run uses mutable runtime access.
  • Runtime macros borrow ctx.run for duration of macro call.
  • Do not use ctx.run again inside with_state_mut!, with_node_mut!, or similar closure.
  • Pull copy data out first (f32, NodeID, ids, bools, enums, small math types).
  • If data owns heap content (String, Vec, Cow, custom clone types), clone out b4 closure if later code still needs it.
  • Clone cost stays local; tmp clone drops aft closure/use site.

Script dependencies:

  • Add extra crates to deps.toml in your project root under [dependencies].
  • On perro check, perro dev, and perro build, Perro merges those entries into .perro/scripts/Cargo.toml.
  • Keep perro managed by Perro; do not override it in deps.toml.

See: