Documentation
Getting Started
Everything you need to start building games with Patina Engine. Build a SceneTree, load Godot scenes, and run them headlessly in Rust.
Installation
Add Patina Engine to your Rust project using Cargo. The engine is distributed as a workspace of focused crates — use only what you need.
[dependencies]
gdscene = { git = "https://github.com/patinaengine/patina" }
gdvariant = { git = "https://github.com/patinaengine/patina" }Patina requires Rust 1.75 or later. We recommend using rustup to manage your Rust toolchain.
# Install Rust (if you haven't already)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Create a new project
cargo init my-game
cd my-gameBuilding a SceneTree
The SceneTree is the heart of the engine. Create one, add nodes, and traverse the hierarchy — just like Godot.
use gdscene::scene_tree::SceneTree;
use gdscene::node::Node;
fn main() {
let mut tree = SceneTree::new();
let root = tree.root_id();
// Add a player node under root
let player = Node::new("Player", "CharacterBody2D");
let player_id = tree.add_child(root, player).unwrap();
// Add a sprite as a child of the player
let sprite = Node::new("Sprite", "Sprite2D");
tree.add_child(player_id, sprite).unwrap();
// Query by path
let found = tree.get_node_by_path("root/Player/Sprite");
assert!(found.is_some());
println!("Tree has {} nodes", tree.node_count());
}Loading .tscn Files
Patina natively parses Godot .tscn files using PackedScene. Parse the text format, then instance the nodes into a SceneTree.
use gdscene::{PackedScene, add_packed_scene_to_tree};
use gdscene::scene_tree::SceneTree;
use gdscene::LifecycleManager;
fn main() {
let tscn = std::fs::read_to_string("main.tscn").unwrap();
let packed = PackedScene::from_tscn(&tscn).unwrap();
println!("Scene has {} nodes", packed.node_count());
// Instance into a live SceneTree
let mut tree = SceneTree::new();
let root = tree.root_id();
let ids = add_packed_scene_to_tree(&mut tree, root, &packed).unwrap();
// Fire lifecycle callbacks (READY, ENTER_TREE)
LifecycleManager::enter_tree(&mut tree, ids[0]);
// Traverse instanced nodes
for id in &ids {
if let Some(node) = tree.get_node(*id) {
println!("{}: {}", node.name(), node.class_name());
}
}
}Headless Runner
The patina-runner binary loads a .tscn file, runs lifecycle callbacks and a configurable number of frames, then dumps the tree state as JSON. Ideal for CI, testing, and server-side logic.
# Run a scene for 10 frames at 60fps (default)
patina-runner level.tscn
# Run 120 frames with custom delta
patina-runner level.tscn --frames 120 --delta 0.016
# Pipe JSON output for inspection
patina-runner level.tscn | jq '.children[0].name'Under the hood, patina-runner uses MainLoop which orchestrates process and physics frames with configurable tick rates — matching Godot's frame stepping model.
use gdscene::MainLoop;
use gdscene::scene_tree::SceneTree;
let tree = SceneTree::new();
let mut main_loop = MainLoop::new(tree);
main_loop.set_physics_ticks_per_second(60);
main_loop.run_frames(120, 1.0 / 60.0);
println!("Ran {} frames", main_loop.frame_count());Variant Type System
Patina implements Godot's Variant — a tagged union that can hold any engine value. Properties on nodes are stored as Variants, enabling dynamic typing while keeping Rust's safety guarantees.
Variant::Nil // null / default
Variant::Bool(bool) // boolean
Variant::Int(i64) // 64-bit signed integer
Variant::Float(f64) // 64-bit float
Variant::String(String) // UTF-8 string
Variant::StringName(StringName) // interned string
Variant::NodePath(NodePath) // scene-tree path
Variant::Vector2(Vector2) // 2D vector
Variant::Vector3(Vector3) // 3D vector
Variant::Rect2(Rect2) // axis-aligned 2D rect
Variant::Transform2D(..) // 2D affine transform
Variant::Transform3D(..) // 3D affine transform
Variant::Color(Color) // RGBA color
Variant::Basis(Basis) // 3×3 rotation/scale
Variant::Quaternion(Quaternion) // quaternion rotation
Variant::Aabb(Aabb) // axis-aligned bounding box
Variant::Plane(Plane) // infinite plane
Variant::Array(Vec<Variant>) // heterogeneous list
Variant::Dictionary(..) // string-keyed mapuse gdscene::node::Node;
use gdvariant::Variant;
use gdcore::math::Vector2;
let mut node = Node::new("Player", "CharacterBody2D");
// Set properties as Variants
node.set_property("speed", Variant::Float(200.0));
node.set_property("position", Variant::Vector2(Vector2::new(100.0, 50.0)));
node.set_property("name", Variant::String("Hero".into()));
// Read them back
let speed = node.get_property("speed");
assert_eq!(speed, Variant::Float(200.0));Patina is in early development
APIs shown here reflect the current engine implementation and may evolve as the engine matures. Follow development on GitHub.