Perro is an experimental, open-source game engine written in Rust, designed as a modern alternative to engines like Unreal, Godot, and Unity.
It focuses on performance, flexibility, and ease of use with a unique multi-language scripting system:
- πΆ Pup DSL β a beginner-friendly, lightweight scripting language that compiles to Rust for native performance.
- π¨ FUR (Flexible UI Rules) β a declarative UI system with layouts, panels, and boxing for easy UI design.
- π Multi-Language Scripts β write gameplay in Pup, C#, TypeScript, or pure Rust β everything transpiles to Rust under the hood.
- π¦ Type-Safe Transpilation β full type checking and casting during code generation.
- β‘ Optimized Release Builds β scripts and assets statically link into your final binary.
- π Decoupled Signal System β global, name-based signals that completely decouple emitters from listeners. Use
on SIGNALNAME() {}shorthand for automatic connections.
Clone the repository and build from source:
git clone https://github.com/PerroEngine/Perro.git
cd perro
cargo run -p perro_devThis launches the Perro Editor in dev mode.
Using the CLI:
# Create a new project (defaults to workspace/projects/ProjectName)
cargo run -p perro_core -- new MyGame
# Or specify a custom path
cargo run -p perro_core -- new MyGame /path/to/projectThis creates a new project structure with:
project.toml- Project configurationres/- Resources folder (scenes, scripts, assets)res/main.scn- Default scene with a "World" Node2D and Camera.perro/- Build artifacts (generated by compiler).gitignore- Ignores generated files
Quick Dev Command (Build + Run):
# Build scripts and run project in one command
cargo run -p perro_core -- --path /path/to/project --devThis automatically:
- Transpiles scripts (Pup/C#/TypeScript β Rust)
- Compiles scripts into a DLL
- Runs the project with hot-reload enabled
Manual Workflow (if you prefer more control):
1. Build Scripts Only:
# Compile scripts only (for testing changes)
cargo run -p perro_core -- --path /path/to/project --scripts2. Run Project:
# Run project in dev mode (injects compiled scripts dynamically)
cargo run -p perro_dev -- --path /path/to/project
OR
cargo run -p perro_core -- --path /path/to/project --runIteration Cycle:
- Make changes to scripts in
res/* - Re-run
--devor just--scripts+perro_dev - Changes are automatically picked up by the running game
- Fast iteration cycle (~1β3s recompile time)
Build Final Release:
# Build complete release (compiles everything statically and strips out console logs)
cargo run -p perro_core -- --path /path/to/project --projectBuild Verbose Release (with console window):
# Build release with verbose output and visible console
cargo run -p perro_core -- --path /path/to/project --project --verboseThis:
- Transpiles all scripts β Rust
- Compiles scripts + project into a single binary
- Embeds assets and scripts statically
- Produces an optimized, distributable executable
- Verbose mode: Removes Windows subsystem flag so console is visible and makes console logs visible (useful for debugging)
Result: A single executable with no external dependencies or DLLs.
- Create a new project using the CLI (see above)
- Write scripts in Pup, C#, TypeScript, or Rust in
res/folder - Design scenes by editing
res/*.scnJSON files - Design UI with FUR files in
res/ - Follow the development workflow above to test and iterate
- Build release when ready to distribute
Pup is Perro's built-in scripting language β simple, readable, and compiles to Rust.
Scripts are defined with the @script directive followed by a name and the node type they extend:
@script Player extends Sprite2D
var speed = 7.5
fn init() {
Console.print("Player is ready!")
set_speed(2.1)
}
fn set_speed(new_speed: float) {
speed = new_speed
}
fn update() {
var delta = Time.get_delta()
self.transform.position.x += speed * delta
}
Perro uses a global, decoupled signal system. Signals are identified by name strings, and any script can listen for any signal without needing a reference to the emitter. This completely decouples signalers from listeners.
The easiest way to handle signals is using the on keyword shorthand. This automatically creates a function and connects it to the signal in init():
@script GameManager extends Node
on start_Pressed() {
Console.print("Start button was pressed!")
// Start the game...
}
on pause_Pressed() {
Console.print("Game paused")
}
The on syntax automatically:
- Creates a function with the signal name
- Adds
Signal.connect("signal_name", function_name)to yourinit()function - No need to manually write connection code!
You can also manually connect signals using Signal.connect(). The new simplified format takes just 2 arguments:
@script Player extends Sprite2D
var enemy: Node2D
var bob: Sprite2D
fn init() {
// Connect to a signal on self (function name as string)
Signal.connect("player_Died", on_player_died)
// Connect to a signal on another node (using any node reference)
Signal.connect("enemy_Defeated", enemy.on_enemy_defeated)
Signal.connect("bob_Pressed", bob.on_bob_pressed)
// You can use any variable that holds a node reference
var my_button = self.get_node("start_button")
Signal.connect("start_Pressed", my_button.on_start_pressed)
}
fn on_player_died() {
Console.print("Player died!")
}
Signal.connect() format:
Signal.connect(signal_name, function_reference)- If
function_referenceis a string or identifier β connects toself - If
function_referenceisnode_var.function_nameβ connects to that node variable (e.g.,bob.functionname,enemy.on_defeated,my_button.on_clicked)
- If
Here's a complete example showing how signals work across different scripts:
FUR UI File (res/ui.fur):
[UI]
[Button id=start]
Start Game
[/Button]
[/UI]
Game Manager Script (res/game_manager.pup):
@script GameManager extends Node
fn init() {
Console.print("Game manager ready, listening for start button...")
}
// Listen for the start button signal (emitted automatically by the button)
on start_Pressed() {
Console.print("Starting the game!")
// Initialize game state, load level, etc.
}
Key Points:
- The button in FUR automatically emits
start_Pressedwhen clicked (based on itsid) - The game manager doesn't need a reference to the button
- The game manager doesn't even need to be in the same scene
- Any script anywhere can listen for
start_Pressedby name - The signal system is completely global and decoupled
This decoupling means you can:
- Have UI buttons that emit signals without any scripts attached
- Have game logic scripts that listen for signals without knowing where they come from
- Easily add new listeners or emitters without modifying existing code
- Test signals independently of their sources
You can write scripts in multiple languages. Languages using Tree Sitter for parsing have their full syntax supported:
- Pup (native DSL, hand-written parser)
- C# (syntax via Tree Sitter CST β Perro AST; not all AST bindings implemented yet)
- TypeScript (yntax via Tree Sitter CST β Perro AST; not all AST bindings implemented yet)
- Rust (direct, no transpilation)
The transpilation pipeline:
- Parse β Tree Sitter CST β Perro AST (or manual parser for Pup)
- Codegen β AST β type-checked Rust
- Compile β Rust β DLL (Dev) or static binary (Release)
- Load β DLL hot-load (Dev) or direct calls (Release)
FUR is Perro's declarative UI system for building layouts and UI panels.
[UI]
[Panel bg=sea-5 padding=4]
[Text font-weight=bold text-color=white text-size=xl]
Hello Perro!
[/Text]
[/Panel]
[/UI]
Current Features:
- Layouts and child layouts
- Panels and boxing
- Styling and padding
See perro_editor/res/fur for real examples of FUR in use.
- Scripts are transpiled to Rust, compiled into a DLL
- Engine loads the DLL at runtime
- Load files from disk
- Make changes β recompile (~1β3s) β see updates instantly without restarting
- All scripts transpile β Rust
- Statically linked into final binary
- Result:
- Single executable (no DLLs, no source included)
- Optimized machine code from LLVM
- Scenes, FUR files, images, etc. are all statically embedded
- Your source scripts are protected
This repository contains the Perro engine source code. To build and work on the engine itself:
- Rust 1.92.0 or later (GNU toolchain required - this is what ships with the editor binary for compilation)
- Cargo
On Windows, Rust defaults to the MSVC toolchain, but Perro requires the GNU toolchain. Here's how to install and set it up:
# Install the GNU toolchain (1.92.0 or later)
rustup toolchain install stable-x86_64-pc-windows-gnu
# Set GNU toolchain as default
rustup default stable-x86_64-pc-windows-gnu
# Verify you're using GNU toolchain
rustc --version
# Should show: rustc 1.92.0 (or later) ... (x86_64-pc-windows-gnu)
# Or verify with rustup
rustup show
# Should show: default toolchain: stable-x86_64-pc-windows-gnuIf you already have Rust installed with MSVC:
# Install GNU toolchain for 1.92.0
rustup toolchain install 1.92.0-x86_64-pc-windows-gnu
# Set it as default
rustup default 1.92.0-x86_64-pc-windows-gnu
# Verify
rustc --versionUpdating an existing GNU toolchain:
# Update to latest stable GNU toolchain
rustup update stable-x86_64-pc-windows-gnu
# Or update your default (if already set to GNU)
rustup update stableperro/
βββ perro_core/ # Core engine (structs, scene, render graph)
βββ perro_dev/ # Dev wrapper binary (loads DLLs, runs projects with --path)
βββ perro_editor/ # Editor game project
β βββ .perro/
β β βββ project/ # Editor project crate
β β βββ scripts/ # Editor scripts crate (contains transpiled rust + builds DLL)
β βββ res/ # Resources (FUR files, scenes, assets, scripts)
βββ examples/ # Example game projects
Open the Editor in Dev Mode:
cargo run -p perro_devBuild the Core Alone:
cargo build -p perro_coreAll projects share a build cache (the main workspace target/ in source mode), so the core only compiles once.
The editors are pinned to specific versions of the toolchain, (eg. 1.0 => 1.92.0), toolchains will NOT always be updated each engine update, as to not clog the end user's system with multiple toolchains they don't need. (1.0 and 1.1 could support the same toolchain, even if users update it only is installed once)
Current Requirements:
- Rust 1.92.0 or later (required for wgpu 28.0.0)
- Default toolchain version: 1.92.0
Project Compatibility:
- Old projects use their original editor version by default
- The Project Manager auto-updates to the latest version
- You can manually upgrade a project to a newer editor version if desired
- Older editor versions remain available for projects that haven't upgraded
- β Basic scripting system (Pup, C#, TS -> Rust pipeline)
- β Type checking and casting during Rust codegen
- β DLL loading & dynamic script loading
- β Static linking of scripts and assets during release
- β FUR layouts, panels, child layouts, and boxing
- β Global decoupled signal system with 500ns dispatch
- π Pup DSL expansion (control flow, standard library)
- π C# & TypeScript AST bindings completion
- π FUR runtime editing & editor viewer
- π Scene editor
- π Asset pipeline
Contributions are welcome! You can work on:
- Engine β
perro_core(rendering, scene, runtime) - Editor β Edit the source code and UI of the editor at
perro_editor/res - Scripting β Pup DSL expansion, transpiler improvements, other language support as needed
- Tooling β build system, asset pipeline
See CONTRIBUTING.md for guidelines.
Perro is licensed under the Apache 2.0 License. See LICENSE for details.
Every developer needs a loyal partner, just like a dog β and that's what Perro means in Spanish.