Behavior and data are separated
Script behavior lives on shared script type methods, while each node instance owns distinct runtime state.
Rust Game Engine
What Perro provides: a Rust-first workflow for building scenes, gameplay, and assets with one consistent runtime model.
Scripting model
Use lifecycle, methods, state, and Variant integration while keeping normal Rust modules and traits.
use perro_api::prelude::*; type SelfNodeType = Node2D; #[derive(Clone, Copy, Variant)]struct MoveTuning { sprint: f32,} #[State]struct ExampleState { #[default = 5] count: i32, #[default = MoveTuning { sprint: 1.8 }] tuning: MoveTuning,} lifecycle!({ fn on_update( &self, ctx: &mut ScriptContext<'_, RT, RS, IP>, ) { let right = key_down!(ctx.ipt, KeyCode::KeyD) as i32; let left = key_down!(ctx.ipt, KeyCode::KeyA) as i32; let axis = (right - left) as f32; let dt = delta_time!(ctx.run); with_node_mut!(ctx.run, SelfNodeType, ctx.id, |node| { node.position.x += axis * dt * 5.0; }); }});Script behavior lives on shared script type methods, while each node instance owns distinct runtime state.
with_state! and with_state_mut! scope data borrows so runtime operations can proceed safely after closure ends.
get_var! and set_var! expose script_vars values from scene data and runtime calls without coupling behavior ownership.
call_method! + params! + Variant conversion make runtime dispatch deterministic and inspectable.
methods!({ fn hit_enemy( &self, ctx: &mut ScriptContext<'_, RT, RS, IP>, enemy_id: NodeID, ) { call_method!(ctx.run, enemy_id, func!("take_damage"), params![10_i32]); set_var!(ctx.run, enemy_id, var!("alerted"), variant!(true)); let health = get_var!(ctx.run, enemy_id, var!("health")); signal_emit!(ctx.run, signal!("enemy_hit"), params![enemy_id, health]); }});Emit named events globally. Connect listeners by signal id and method id.
methods!({ fn send_ping( &self, ctx: &mut ScriptContext<'_, RT, RS, IP>, ) { signal_emit!(ctx.run, signal!("ping")); }});lifecycle!({ fn on_init( &self, ctx: &mut ScriptContext<'_, RT, RS, IP>, ) { signal_connect!(ctx.run, ctx.id, signal!("ping"), func!("on_ping")); }});Query system
name[...]Match one or more scene node names.
tags[...]Match nodes carrying runtime tags.
is[...] / is_type[...]Match exact concrete node types.
base[...] / base_type[...]Match nodes that inherit from a base type.
let enemies = query!( ctx, all( any(tags["enemy"], name["Boss"]), base_type[Node3D], not(tags["dead"]) ), in_subtree(arena_root)); for id in enemies { let _ = with_base_node_mut!(ctx, Node3D, id, |node| { node.position.y += 0.1; });}More systems
Filter scene nodes by name/tag/type/base and compose all/any/not logic.
Signals, physics, animation control, script call dispatch, helpers, and timing.
Audio, textures, meshes, materials, localization, draw2d, and clip loading APIs.
Keys and mouse first, gamepads and Joy-Con next, players abstraction for stable bindings.