Rust Scripting, Simplified
Perro Engine
Script gameplay directly in Rust with runtime helpers that keep state and node access safe, straightforward, and clean.
Rust Scripting with Direct Runtime Access
Perro scripts stay in Rust end to end. Runtime helpers remove repetitive plumbing so you focus on behavior while the engine keeps state/node access, lifecycle wiring, and cross-script communication consistent.
Lifecycle Without Boilerplate
lifecycle! declares engine entry points like on_init and on_update in one predictable block so script flow is easy to read.
Methods You Can Call Locally or Remotely
methods! defines regular behavior methods and cross-script call targets. call_method! can invoke those methods by id.
State Access Through Closures
with_state! and with_state_mut! give scoped access to script state so reads and mutations stay explicit and memory-safe.
Typed Node Access In Place
with_node! and with_node_mut! let scripts read and mutate attached nodes directly through typed closures, no unsafe extraction.
Signal Routing Anywhere
emit_signal! broadcasts events from any script, and connect_signal! listens from any other script. No manual reference graph needed.
Fast Script Iteration
Script compile times typically land around 0.5 to 2 seconds, so you can edit behavior and test quickly.
Node Model Is Consistent
Node2D is the base for 2D nodes and Node3D is the base for 3D nodes. SceneNode metadata tracks id, name, parent_id, and children_ids.
Rust as a Scripting Language
Author scripts in Rust with macros that abstract runtime complexity and keep your code safe and clean.
use perro_context::prelude::*;use perro_core::prelude::*;use perro_ids::prelude::*;use perro_modules::prelude::*;use perro_scripting::prelude::*; // Node type this script is authored against.type SelfNodeType = Node2D; // State is data-only and mutated through macro closures.#[State]pub struct ExampleState { #[default = 5] count: i32,} const SPEED: f32 = 5.0; lifecycle!({ fn on_init(&self, ctx: &mut RuntimeContext<'_, R>, self_id: NodeID) { let count = with_state!(ctx, ExampleState, self_id, |state| state.count) .unwrap_or_default(); log_info!(count); } fn on_update(&self, ctx: &mut RuntimeContext<'_, R>, self_id: NodeID) { let dt = delta_time!(ctx); with_node_mut!(ctx, SelfNodeType, self_id, |node| { node.position.x += dt * SPEED; }); self.bump_count(ctx, self_id); }}); methods!({ fn bump_count(&self, ctx: &mut RuntimeContext<'_, R>, self_id: NodeID) { with_state_mut!(ctx, ExampleState, self_id, |state| { state.count += 1; }); }});Global Signal System
Emit a signal from anywhere, and listen to that same signal from anywhere else. You do not need direct references between systems, scenes, or UI.
methods!({ fn send_ping(&self, ctx: &mut RuntimeContext<'_, R>, _self_id: NodeID) { emit_signal!(ctx, sig_id!("ping")); }});lifecycle!({ fn on_init(&self, ctx: &mut RuntimeContext<'_, R>, _self_id: NodeID) { connect_signal!(ctx, sig_id!("ping"), func_id!("on_ping")); }}); methods!({ fn on_ping(&self, _ctx: &mut RuntimeContext<'_, R>, _self_id: NodeID) { log_info!("received ping"); }});Build with Rust, Stay Focused on Gameplay
Perro keeps scripting practical for beginners and fast for experienced teams by abstracting runtime complexity behind consistent macros and typed engine access.
