Patina

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.

Cargo.toml
[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.

Terminal
# 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-game

Building a SceneTree

The SceneTree is the heart of the engine. Create one, add nodes, and traverse the hierarchy — just like Godot.

main.rs
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.

main.rs
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.

Terminal
# 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.

Programmatic usage
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.

Supported Variant types
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 map
Using Variants on nodes
use 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.