Perro is an experimental, open-source game engine written in Rust. With a focus on performance and simplicity without sacrificing either.
- Simple To Learn: start with scenes, nodes, and Rust scripts without large registration steps or boilerplate.
- Flexible To Use: reduces borrow-checker friction with direct APIs and scoped closures for mutable access. Separating state from script behavior prevents runtime borrow failures.
- Fast In Release: nodes and scripts are laid out for efficient node and state access, all resources are statically baked in release for efficient and quick retrieval
- Full Game-Making Scope: 2D, 3D, and UI all matter. Perro aims to support both 2D and 3D performantly, with high frame rates and a workflow that stays simple.
- Simple Start: get first scene and script running quickly, with minimal setup and no script-registration boilerplate.
- Compiler-Managed Workflow: let Perro sync scripts, generate glue code, and prepare supported assets so project setup stays small.
- Split Model: scripts are just Rust files (lifecycle + methods); they store #[State] structs which each instance gets a copy of.
- Safe Mutation: access through
NodeIDclosures and engine-managed storage avoids borrow-contention edge cases in normal gameplay code (no "try_get_mut" fails). - Fast Access: flat ID lookups keep common node/script operations efficient, with room to cache IDs for hot paths.
- Quick Iteration: project scripts build and reload in usually less than 1 second after initial compilation.
Project scripts use a small, predictable shape. Put per-instance data in a
#[State] struct, lifecycle callbacks in lifecycle!, and methods in
methods!:
#[derive(Default, Variant)]
struct MotionState {
speed: f32,
}
#[State]
struct GameState {
score: i32,
motion: MotionState,
}
lifecycle!({
fn on_update(&self, ctx: &mut ScriptContext<'_, API>) {
self.internal_method(ctx);
}
});
methods!({
// GameState methods
fn internal_method(&self, ctx: &mut ScriptContext<'_, API>) {
// private, lifecycle-local logic
}
pub fn externally_callable_method(&self, ctx: &mut ScriptContext<'_, API>) {
// externally callable entry point
}
});The state type may use any clear name. Group related values in nested structs
when that keeps one script readable. Keep same-script helpers private with
fn; mark methods pub fn only when other scripts, signals, or generated
dispatch need to call them.
Good: keep durable per-node fields in #[State]; group related fields in nested structs.
Good: call private methods from lifecycle callbacks; keep free helpers pure.
Bad: pass ScriptContext or ScriptAPI into free functions; use lifecycle or methods instead.
Bad: default gameplay state to Mutex, RefCell, or thread_local!.
Author scene topology, child nodes, script attachments, refs, and defaults in
composable .scn files. Do not construct authored scene trees in gameplay code.
Use runtime scene APIs to load or instantiate authored .scn assets when runtime
composition needs them. Engine internals, tests, and editor/tooling may build
nodes when their job requires it.
Treat res/**/*.rs and .scn files as source. Treat .perro/ generated glue
as output: inspect it for diagnosis, never edit it as source.
For more details, see the full documentation: perroengine.com/docs.
Local reference:
- Docs Index
- Engine architecture + extension paths
- WASM / Web Target
- ResPath
project.toml- Feature Matrix
- Performance + Flexibility Philosophy
- Perro CLI
cargo check --workspace --all-targetscargo test --workspacecargo clippy --workspace --all-targets -- -D warnings -F clippy::all
- Behavior Scripts + Per-Node State: a script is function entry points (lifecycle hooks + methods), not a mutable behavior object. When a node binds that script, runtime uses that nodeโs
ctx.idto run behavior and resolve that nodeโs own#[State]viawith_state!/with_state_mut!. - Object-Centric Scene Model: parent/child relationships, concrete node types, and traditional game-object structure stay front and center.
- Compiler-Backed Asset Flow: dev stays flexible with plain files, while build/export bakes supported assets into fast static lookup paths and packs the rest.
- Powerful UI System: UI is built as a real engine system with relative sizing, clamping, and layouts designed to scale from simple menus to larger game interfaces.
- Flat ID-Based Runtime Access: node and script data are addressed by
NodeID, enabling constant-time lookups for common operations and efficient cross-system interaction. - Predictable Failure Modes: most runtime misses come from real-world state changes (deleted node, missing tag/name match, unbound script), not from borrow contention between unrelated systems (no
try_get_mutruntime errors). - Powerful Query Layer: if you prefer query-style access, filter by type, base type, tag, name, and subtree to gather
NodeIDs, then operate directly through script/node APIs. See Query System.
Perro is, of course, open source, and contributions are always appreciated: issue reports, new features, system optimizations, and other improvements. Everyone is welcome to join the project.
Donations help fund full-time development, faster features, and better tooling. If you want to support the project:
Perro is licensed under the Apache 2.0 License. See LICENSE for details.