| 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 |
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.
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.
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.
| 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 |
| 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.
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).
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 structlifecycle!for engine entry pointsmethods!for callable behavior methods- bare Rust modules for shared code (
res/**.rswith no script behavior) - script contexts (
RuntimeWindow,ResourceWindow,InputWindow)
Borrow rule:
ctx.runuses mutable runtime access.- Runtime macros borrow
ctx.runfor duration of macro call. - Do not use
ctx.runagain insidewith_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.tomlin your project root under[dependencies]. - On
perro check,perro dev, andperro build, Perro merges those entries into.perro/scripts/Cargo.toml. - Keep
perromanaged by Perro; do not override it indeps.toml.
See:
- Script Authoring Guide
- Project Script Modules
- Parallel Jobs
- Script Contexts
- Script Utility Modules
- Struct Types
- Node Types
- Physics Nodes
- Audio Nodes
- Water Bodies
- Node Collections
- Runtime-only batches, generated nodes, and
create_nodes!; author fixed composition in.scnfiles.
- Runtime-only batches, generated nodes, and
- Script State
- Script Lifecycle
- Script Methods
- Variant