Rust Scripting, Simplified

Perro Engine

Script gameplay directly in Rust with runtime helpers that keep state and node access safe, straightforward, and clean.

Perro Engine Logo

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.

Emitter
📡
Receiver
📻
Emitter
methods!({    fn send_ping(&self, ctx: &mut RuntimeContext<'_, R>, _self_id: NodeID) {        emit_signal!(ctx, sig_id!("ping"));    }});
Receiver
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.