Reference

Ruzta documentation

(roo.stuh)

Section 1

Setup

Ruzta is a GDExtension for Godot. Download the archive or itch.io or godot asset store, extract it into your project, and attach `.rz` scripts to nodes just like GDScript.

Download and enable Ruzta

Ruzta scripts live in `.rz` files, but the language itself is delivered as a Godot GDExtension.

hello_world.rz
extends Nodefunc _ready() -> void:	print("Hello from Ruzta")
  • Download the current build archive, then unzip it before opening the project in Godot.
  • A Ruzta source file uses the `.rz` extension
  • If Godot does not recognize the language immediately, reopen the project so the extension reloads cleanly.

First `.rz` script

If you already know Godot scenes, verify your setup works by attaching a `.rz` script to a node and printing from `_ready()`.

player.rz
class_name Playerextends CharacterBody2D@export var speed := 220.0func _ready() -> void:	print("Player ready")func _process(delta: float) -> void:	position.x += speed * delta
  • Create a new script file with the `.rz` extension.
  • Use `extends` exactly the way you would in GDScript.
  • Attach the script to a node, run the scene, and check the output panel for the printed line.
  • From there, add `@export` properties and callbacks such as `_process()` or `_input()` the same way you would in a regular Godot workflow.

Getting Started with Ruzta Tools

Ruzta ships with an editor plugin that provides GDScript-to-Ruzta conversion tools and a built-in MCP server. The MCP server starts on `localhost:7778/mcp` and currently provides a single `validate-ruzta-script` MCP tool. You can extend `RuztaMCPTool` to add more tools.

To enable Ruzta Tools, go to Project > Project Settings > Plugins, find Ruzta Tools, and check Enabled.

You can then find the available tools under the Project > Tools menu in the editor.

Learn more

Ruzta is intentionally close to GDScript, so the quickest way to learn is to combine this site with the official Godot scripting references and a couple of practice-oriented guides.

  • Keep this Ruzta guide open for the repo-specific differences: `.rz` files, language packaging, and how this port is positioned.
  • If you want a guided path instead of a reference manual, start with GDQuest's beginner material and cross-check syntax here.

Section 2

Ruzta vs GDScript

Ruzta uses the same Godot scripting model as GDScript but adds tuples, generics, traits, function overloading, payload enums, and builder Constructor. Files use `.rz` instead of `.gd`.

What stays the same

Most scripting works the same: indentation rules, `extends`, `class_name`, type hints, signals, annotations, collections, control flow, and the general Godot scripting model.

shared_shape.rz
class_name Doorextends Node2D@export var is_open := falsefunc toggle() -> void:	is_open = not is_open
  • If you can read GDScript, you can read most Ruzta immediately.
  • Static typing uses the same mental model: optional type hints, typed arrays, typed dictionaries, return annotations, `is`, and `as`.
  • Scene callbacks such as `_ready()`, `_process()`, `_physics_process()`, and signal handlers look the same.
  • Inspector workflows built around `@export`, `@onready`, and autoload singletons carry over as well.

What changes in practice

The biggest differences are distribution and release timing, not everyday syntax. Ruzta scripts use `.rz`, and this project ships the language runtime as a GDExtension rather than baking it into the editor build.

  • Use `.rz` instead of `.gd` for source files.
  • Official Godot pages often talk about GDScript by name; for syntax and most patterns, those pages are still relevant to Ruzta.
  • For edge cases, prefer testing against the actual Ruzta runtime because language ports can move on a different release cadence than upstream Godot.
  • If a guide mentions a GDScript builtin or annotation, assume it is conceptually relevant, then confirm the exact behavior in Ruzta when it matters.
  • Ruzta uses compact range syntax (`start..end`, `start..=end`, `start..end:step`) instead of `range(start, end)` — steps and reverse direction are inferred from bounds.
  • Postfix `value++` and `value--` are supported for compact mutation; GDScript requires `value += 1`.
  • Annotation blocks can be scoped with `@annotation:` followed by an indented block, applying to every compatible declaration inside.
  • Variadic functions use `...args: Array` syntax instead of GDScript's `args: Array = []`.

What Ruzta adds

Beyond the shared GDScript model, Ruzta introduces several features that have no direct equivalent in GDScript. These are entirely new tools you can reach for when a pattern or architecture calls for them.

ruzta_additions.rz
tuple Vec2(x: float, y: float)trait Damageable:	func take_damage(amount: int) -> void@generic(T)class Wrapper:	var value: T	func _init(v: T):		value = v
  • Tuples define lightweight, fixed-size aggregate types with named or positional fields, destructuring, and match patterns — useful for small data carriers without a full class.
  • Generics with `@generic(T)` let you write reusable classes and functions parameterized over types, backed by call-site inference and constraints.
  • Traits (declared with `trait` and applied with `uses`) provide interface-plus-mixin capabilities that decouple shared behavior from inheritance.
  • Function overloading allows multiple definitions sharing one name when their parameter types differ, resolved by best signature fit.
  • Payload enums act as tagged unions — each case can carry its own structured data while still being a single enum type.
  • Builder constructors (`TypeName { ... }`) let you construct and configure a node subtree inline with control flow and automatic `add_child()` calls.
  • `@feature` / `@feature_any` gate declarations and blocks behind platform or editor feature flags, stripped at runtime when inactive.
  • Named argument calls use `fn(param: value)` or `fn(value, param=value)`.

Ruzta - GDScript Interoperability

To use GDScript classes in a Ruzta script you will want to use `load("<gdscript file path>")` syntax, same applies in GDScript.

modify_style.rz
func change_error_btn_color():	load("res://addons/gdss/gdss.gd").set_prop_override(error_toggle_btn, "font_color", Color.RED)

Section 3

Variables and Constants

Use `var` for mutable state and `const` for immutable values. Ruzta supports static variables and type inference with `:=`.

Declaring values

values.rz
const DEFAULT_SPEED := 240.0const ENEMY_SCENE = preload("res://enemy.tscn")var health := 5var title: String = "Scout"var active := true
  • Use `=` when you want a simple assignment and `:=` when you want inference from the initializer.

Scope and static state

Ruzta also supports `static var` and `static func` for data or helpers that belong to the class itself instead of an instance.

static_values.rz
static var spawned_count := 0var nickname := "unit"func _init() -> void:	spawned_count += 1static func get_spawned_count() -> int:	return spawned_count

Section 4

Primitive Types

Ruzta uses Godot's standard types: `bool`, `int`, `float`, `String`, `StringName`, `NodePath`, and math types like `Vector2`. Use `is` for type checks and `as` for casts.

Core value types

primitive_values.rz
var retries: int = 3var cooldown: float = 0.35var label: String = "Ruzta"var enemy_name: StringName = &"Enemy"var camera_path: NodePath = ^"Player/Camera2D"var position_2d := Vector2(16, 32)
  • `bool`, `int`, and `float` cover most gameplay flags, counters, timers, and movement values.
  • `String` is the everyday text type; `StringName` is useful for identifiers, property names, and other interned keys.
  • `NodePath` is the typed representation behind scene paths, and math/value structs such as `Vector2`, `Vector3`, `Color`, and `Transform3D` work the same way as in GDScript.
  • `null` is valid for `Variant` and object-like references where an empty value makes sense.

Casts and type checks

Use `is` when you want to test a value's runtime type and `as` when you want to cast it into a more specific type. See the static typing guide for details.

casts.rz
func describe(value: Variant) -> void:	if value is int:		print("int:", value)	elif value is String:		print("string:", value)	var amount := value as int
  • Use `is` in guards before touching members on dynamic values.
  • Use `as` when the conversion is intentional and should produce a typed result.
  • Typed arrays and dictionaries can also participate in type tests such as `value is Array[int]`.

Section 5

Aggregate Types

Ruzta supports typed `Array`, typed `Dictionary`, and new `tuple` types. Tuples are fixed-size values with named or positional fields, destructuring, and match patterns.

Array

Arrays are ordered collections. Use them for sequences, inventories, waypoints, batched events, or any other list-shaped data.

arrays.rz
var checkpoints: Array[Vector2] = [Vector2.ZERO, Vector2(64, 0)]checkpoints.append(Vector2(128, 0))for point in checkpoints:	print(point)
  • Untyped arrays are flexible and useful for quick gameplay scripting or dynamic data.
  • Typed arrays such as `Array[int]` or `Array[Enemy]` give stronger editor help and earlier errors.
  • When the element type is a class or script, compatible subclasses are valid elements too, so `Array[Enemy]` can store `BossEnemy` instances.
  • Nested typed arrays are supported, for example `Array[Array[int]]`.
  • Loop arrays directly with `for item in items:` or by index when you need positional access.

Dictionary

Dictionaries store key-value pairs. Use them for named stats, lookup tables, metadata blobs, and other record-like structures where labels matter more than order.

dictionary.rz
var stats: Dictionary[String, int] = {	"hp": 8,	"mp": 3,}stats["hp"] += 1for key: String in stats:	print("%s = %d" % [key, stats[key]])
  • Untyped dictionaries are convenient for quick configuration data and deserialized content.
  • Typed dictionaries such as `Dictionary[String, int]` or `Dictionary[int, LootDrop]` are better when a shape is expected.
  • When keys or values are typed as classes or scripts, compatible subclasses are valid too, including subclass instances used as dictionary keys.
  • Nested typed dictionaries and mixed nested containers are supported, for example `Dictionary[String, Array[int]]`.
  • Iterating a dictionary yields keys; use those keys to read or update the stored values.

Tuple

Tuples are fixed-size aggregate values. You can declare named tuple types with `tuple Name(...)`, create unnamed tuple literals with `(a, b)`, access elements by index (`.0`) or field name (`.name`), and destructure values into local bindings.

tuples.rz
tuple Player(	name: String,	hp: int,	alive: bool)tuple Vec2(	x: float,	y: float)func get_data() -> (String, int):	return ("Coins", 50)func test() -> void:	var a := Vec2(10.0, 20.0)	var b := Vec2(x: 2.0, y: 3.0)	print(a.0, a.1)	print(b.x, b.y)	var pos: (int, int) = (10, 20)	var (x, y) = pos	print(x, y)	match pos:		(0, var any_y):			print("x is zero", any_y)		(var any_x, 0):			print("y is zero", any_x)		(var any_x, var any_y):			print(any_x, any_y)
  • Declare tuple types with named, unnamed, or mixed fields: `tuple Vec2(x: float, y: float)` and `tuple Player(name: String, int, bool)` are both valid.
  • Tuple literals use parentheses with commas: `(10, 20)`; single parentheses without a comma remain expression grouping.
  • Use tuple type annotations in variables and return types, for example `var pos: (int, int)` and `func get_data() -> (String, int)`.
  • Destructure declarations with `var (x, y) = pos` or `const (name, hp) = get_player()`.
  • Tuple index access works on all tuples (`value.0`, `value.1`), while named access (`value.x`) works only on tuple types that declare those names.
  • Tuple values can nest naturally, participate in `match` tuple patterns, and follow constant immutability rules (`const pos = (10, 20)` rejects `pos.0 = 5`).

Section 6

Functions, Lambdas, and Setters/Getters

Define functions with `func`. Ruzta adds function overloading, named arguments.

Functions and return values

Define behavior with `func`. Parameters go in parentheses, defaults keep call sites short, and `->` documents the return type when a function produces a value.

functions.rz
func heal(amount: int = 1) -> void:	health += amountfunc is_alive() -> bool:	return health > 0

Function overloading

Ruzta allows multiple functions to share one public name as long as their parameter signatures differ. Calls are resolved deterministically by best signature fit, including generic candidates that can infer their type arguments from the call.

function_overloading.rz
class Tool:	func _init(id: String) -> void:		print("string init:", id)	func _init(id: int) -> void:		print("int init:", id)	static func pick(value: int) -> String:		return "int"	static func pick(value: String) -> String:		return "string"func test() -> void:	var a := Tool.new("x")	var b := Tool.new(3)	print(Tool.call("pick", 7))	var picker := Tool.pick	print(picker.call("ok"))
  • Overloading works for global functions, class methods, static methods, and constructors (`_init`).
  • Resolution prefers exact type/shape matches before candidates that require implicit conversion.
  • Overloads that differ only by return type are rejected.
  • Named arguments are checked per candidate before final overload selection.
  • Reflective calls (`call`, `callv`) and callable references dispatch through the same overload rules at runtime.

Named arguments

Function and method calls can mix positional and named arguments. Positional arguments still bind left to right, while named arguments bind by parameter name.

named_arguments.rz
func create_user(name: String, age: int, admin: bool = false) -> void:	print(name, age, admin)create_user("Xkai", admin: true, age: 22)create_user(name: "Blue", age: 19)
  • Write named arguments as `parameter_name: value` inside the call.
  • Positional arguments must come first, then named arguments, then any variadic tail values.
  • Named arguments can reorder the remaining supplied parameters, which helps at call sites with several same-shaped values.
  • Default arguments still follow the usual trailing omission rule, so you cannot skip an earlier parameter and then provide a later one.
  • With overloaded functions, named-argument compatibility is checked per overload candidate before type-fit scoring.
  • If several overloads remain equally valid after named-argument and type-fit checks, the call is reported as ambiguous.

Lambdas and callables

Lambdas are inline functions created with `func`. They are useful for short callbacks, custom sort logic, deferred work, and signal handlers that do not deserve a named method.

lambdas.rz
var announce := func(message: String) -> void:	print("announce:", message)var double := func(value: int) -> int:	return value * 2announce.call("ready")print(double.call(4))
  • Store lambdas in variables or pass them directly where a `Callable` is expected.
  • A lambda can declare parameter types and a return type just like a named function.
  • Call lambdas with `.call(...)`.

Setters and getters

Properties can expose a computed interface instead of a raw backing field. Inline `get:` and `set(value):` blocks are the clearest form when the logic is small.

properties.rz
var _speed := 200.0var speed: float:	get:		return _speed	set(value):		_speed = maxf(0.0, value)
  • Use a private backing variable when the property should validate or normalize writes.
  • For larger property logic, you can also route through named getter and setter functions.
  • Typed properties work the same way as untyped ones.

Variadic arguments

Ruzta supports variadic parameters with `...args: Array`. Use them when the function really accepts a flexible tail of values.

variadic.rz
func log_event(name: String, level: int = 0, ...args: Array) -> void:	prints(name, level, args)var collector := func(prefix: String, ...args: Array) -> void:	prints(prefix, args)log_event("spawn")log_event("damage", 2, "orc", 15)collector.call("values", 1, 2, 3)
  • Keep required parameters first, optional defaults next, and the variadic tail last.
  • The collected extra arguments arrive as an array.
  • Variadics also work in lambdas, so short forwarding helpers can stay inline.

Section 7

Enum

Ruzta supports plain enums and payload enums (tagged unions). Payload enums let each case carry different data, with destructuring in `match` patterns.

Plain named and unnamed enums

Plain enums group related integer constants under a readable name. Use named enums for most public APIs and unnamed enums when you want a few file-local constants without an extra type name.

enums.rz
enum Direction { LEFT = -1, RIGHT = 1 }enum { STARTING_LIVES = 3 }var facing: Direction = Direction.RIGHTvar lives := STARTING_LIVES
  • A named enum is accessed through its type, such as `Direction.LEFT`.
  • Unnamed enum entries are introduced directly into the surrounding scope.
  • You can assign explicit numeric values when you need stable save data, wire formats, or editor-facing identifiers.

Payload enums / tagged unions

If any enum case declares payload, the whole enum becomes a tagged union instead of an int-backed enum. Use this when each case needs to carry different data but you still want one shared enum type.

payload_enum.rz
enum Message {	Quit,	Move(x: int, y: int),	Write(text: String),}func handle_message(msg: Message) -> void:	if msg is Message.Move(x, y):		prints("preview move", x, y)	match msg:		Message.Quit:			print("quit")		Message.Move(x, y):			prints("move", x, y)		Message.Write(_):			print("write")
  • Construct payload cases like `Message.Move(x: 4, y: 9)` or, for unnamed enums, `Ping(id: 3)` inside the declaring scope.
  • Destructure payload with `if value is Message.Move(x, y)` or directly in `match` patterns.
  • Use `_` inside a payload pattern when you want to ignore that payload, such as `Message.Write(_)`.
  • Payload enums may declare explicit numeric case values and can be cast to `int`, which keeps only the case tag and discards payload.
  • A payload enum case pattern must stand on its own branch; do not combine it with `,`-separated alternatives.

Using enums in typed code

Use enums when a variable should stay inside one finite domain. Plain enums compare like named integer states, while payload enums let each case carry structured data without leaving the enum type.

enum_usage.rz
enum State { IDLE, RUN, HIT, DOWN }func set_state(state: State) -> void:	match state:		State.IDLE:			print("idle")		State.RUN:			print("run")		_:			print("other")
  • Use enum types for variables, parameters, return values, and dictionary keys when a state machine or finite set is involved.
  • Enums work well in `match` expressions because every branch reads like a named state or case instead of a magic number.
  • If you inherit or preload scripts, enum members can also be accessed through those script types.

Section 8

Control Flow

Standard `if`/`elif`/`else`, `for`/`while` loops, `match` with pattern matching and guards, plus `break`/`continue`/`return`/`pass`.

Conditionals

Use `if`/`elif`/`else` for branching, with an inline ternary form for compact value selection.

conditionals.rz
if health <= 0:	state = "down"elif sprinting:	state = "run"else:	state = "idle"var banner = "danger" if health < 3 else "safe"
  • Reach for `if` blocks when each branch performs multiple actions.
  • Use the inline `a if condition else b` form when you only need to pick one value.

Pattern matching

Use `match` when the branching logic is state-based or pattern-based. It reads better than a long `if` chain once values, destructuring, or guards are involved.

match.rz
match state:	"idle":		print("standing")	"run":		print("moving")	var current when current.begins_with("attack"):		print("combat")	_:		print("unknown")
  • Use `_` as the fallback pattern.
  • Pattern guards with `when` let you refine a matching branch without leaving the `match` block.
  • Arrays and other structured values can be matched destructively, including variable binds inside the pattern.

Loops

Use `for` when iterating a range or iterable value and `while` when the stop condition depends on state that changes inside the loop.

loops.rz
for index in 0..=3:	print(index)for action in ["jump", "dash", "roll"]:	print(action)while energy > 0:	energy -= 1
  • Loop arrays, strings, dictionaries, and custom iterables with `for item in value:`.
  • Use `range(start, end, step)` when you need index-style iteration.
  • Use range syntax when you want the compact loop form: `start..end`, `start..=end`, or `start..end:step`.
  • When the step is omitted, the direction is inferred from the bounds, so `10..0` walks backward automatically.
  • A dictionary loop yields keys, not key-value tuples.
  • Keep `while` loops tight and make the exit condition obvious so they do not turn into hidden infinite loops.

Section 9

Operators

Standard arithmetic, comparison, and boolean operators plus postfix `++`/`--`. Ruzta adds compact range syntax (`0..10`, `10..0`) and type membership with `is`/`as`/`in`.

Arithmetic, comparison, and assignment

The core operator set is the familiar one: arithmetic, comparisons, boolean logic, compound assignments, and a couple of small mutation shorthands.

operators.rz
score += 10ammo--combo++var can_dash = stamina > 0 and not exhaustedvar same_lane = lane_a == lane_bvar wrapped = turn % 4
  • Use `+`, `-`, `*`, `/`, and `%` for numeric work.
  • Use `==`, `!=`, `<`, `<=`, `>`, and `>=` for comparisons.
  • Use `and`, `or`, and `not` for boolean logic.
  • Compound assignments such as `+=`, `-=`, `*=`, `/=`, and `%=` keep mutations compact.
  • Use postfix `value++` and `value--` as shorthand for `value += 1` and `value -= 1`.
  • Treat `++` and `--` like assignment statements: they mutate a target rather than producing a separate expression value.

Range Syntax

Ruzta supports compact range literals for exclusive bounds, inclusive bounds, reverse iteration, and custom steps without forcing every use through a `range(...)` call.

range_syntax.rz
print(0..10)print(0..=10)print(0..10:2)print(10..0)print(10..=0)print(0..2 + [3, 4])
  • Use `start..end` for an exclusive upper bound.
  • Use `start..=end` when the final value should be included.
  • Use `start..end:step` when the stride is not the default `1`.
  • When the step is omitted, the direction is inferred from the bounds, so `0..10` uses `1` while `10..0` uses `-1`.
  • Negative steps still let you force reverse traversal or larger jumps, such as `10..0:-2`.
  • Keep range bounds and steps as bare variables or integer literals; precompute more complex values first.
  • Syntax ranges still evaluate to arrays, so array operators such as `+` continue to work on them.

Type and membership operators

A few operators matter especially often in script code: `is`, `as`, and `in`.

type_ops.rz
if target is Node2D:	print(target.position)var named_target := target as Nodeif "dash" in abilities:	print("dash ready")
  • Use `is` for runtime type checks before touching object members or narrowing a dynamic value.
  • Use `as` when a typed conversion is intentional and should be visible in the code.
  • Use `in` to test whether a value exists in an array, string, or other container-like type.

Section 10

Printing and String Formatting

Use `print`, `prints`, and `printerr` for output. Supports percent-style string formatting (`%d`, `%s`, `%02f`) for debug messages and status text.

Console output and formatted strings

printing.rz
print("ready:", player_name)prints("spawn", wave, position)printerr("Missing save file")var hp_label = "HP %03d / %03d" % [health, max_health]var time_label = "Time %.02f" % elapsedprint(hp_label)print(time_label)
  • Use `print` for general logging and `printerr` when you want the message to stand out as a problem.
  • Use `prints` when you want several values separated cleanly without building a string first.
  • Use the `%` formatter for width, padding, decimal precision, and multi-value templating.
  • Prefer readable format strings over long chains of concatenation when building debug messages.

Section 11

Signals and Concurrency

Declare signals with `signal`, connect with `connect()`, emit with `emit()`. Use `await` for async flow control with signals and coroutines.

Signals

Signals decouple systems cleanly. Declare with `signal`, connect with `connect()`, and emit with `emit()`.

signals.rz
signal collected(item_name, amount)func _ready() -> void:	collected.connect(_on_collected)func pickup(name: String, amount: int) -> void:	collected.emit(name, amount)func _on_collected(item_name: String, amount: int) -> void:	print(item_name, amount)
  • Custom signals are great for UI updates, combat events, scene transitions, and gameplay milestones.
  • A signal can declare parameters, which makes the payload contract explicit.
  • Typed signal parameters also drive editor help for `emit(...)` calls and typed `connect(...)` handlers.
  • You can connect a named method or an inline lambda depending on how much logic the handler needs.

Typed signal emit and connect checks

When a `Signal` type has a known signature, the editor validates `emit(...)` arguments and `connect(...)` callables against that signal shape.

typed_signals.rz
signal announced(value: int, ignored: String)func _ready() -> void:	var handler: Callable = func(value: int) -> void:		print("lambda:", value)	announced.connect(handler.unbind(1))	announced.connect(_on_tagged.bind("ui"))	var forwarded := Signal(self, "announced")	forwarded.emit(3, "drop")func _on_tagged(value: int, _ignored: String, tag: String) -> void:	print(tag, value)
  • Typed `signal name(value: Type, ...)` declarations drive argument checking for `signal.emit(...)`.
  • The same typed signal metadata flows through `Signal(self, "name")` when the signal name is statically known.
  • Typed `connect(...)` checks apply to named methods, typed lambdas stored in `Callable` variables, and `Callable.bind(...)` or `Callable.unbind(...)` chains when the callable shape is still knowable.
  • These are editor-time checks. Runtime signal behavior and legacy string-based signal APIs stay unchanged.

Await and asynchronous flow

Use `await` to suspend a function until a signal or coroutine result is ready. This keeps async scene logic readable without manually threading state through callbacks.

await.rz
signal finished(result)func _ready() -> void:	call_deferred("emit_finished")	var result = await finished	print(result)func emit_finished() -> void:	finished.emit("done")
  • Await a signal directly with `await some_signal`.

Section 12

Class and Global

Use `class_name` for global identifiers, `extends` for inheritance, and autoload singletons for project-wide services.

Unnamed scripts and `class_name`

Every `.rz` file defines a script class. Adding `class_name` gives the class a global identifier you can reference directly.

classes.rz
class_name Projectileextends Area2Dconst EnemyScript = preload("res://enemy.rz")func spawn_enemy() -> void:	var enemy = EnemyScript.new()	add_child(enemy)
  • Use `class_name` when the script should be easy to instantiate or reference across the project.
  • Unnamed scripts still work fine; load or preload them and instantiate them through the returned script resource.
  • Use `extends` at the top level to bind the script to a native or script base class.

Inner classes

A Ruzta script can also declare inner classes. They are useful when a helper type belongs tightly to one script and does not need its own global file or `class_name`.

inner_class.rz
class Entry:	var id: String	var count: intfunc make_entry(id: String, count: int) -> Entry:	var entry := Entry.new()	entry.id = id	entry.count = count	return entry
  • Inner classes help keep small data carriers and helper objects local to the script that owns them.
  • They can extend other classes and participate in typed code just like top-level classes.
  • Use them when splitting files further would make the code harder, not easier, to navigate.

Globals and singletons

Autoload singletons are the usual Godot answer for project-wide services and state. Once registered in Project Settings, they are available by name from any Ruzta script.

singletons.rz
func _ready() -> void:	if SaveGame.has_profile():		print(SaveGame.current_slot)		SaveGame.mark_seen("intro")
  • Use an autoload for save systems, settings, audio routers, quest state, or other cross-scene services.
  • A singleton name behaves like a global entry point, but the underlying implementation is still just a script or node you own.

Section 13

Traits

Traits provide interface-plus-mixin capabilities. Declare with `trait`, apply with `uses`. Traits can have required methods, default implementations, and support `is`/`as` checks.

Declaring traits

A trait can declare required methods like an interface, include implemented members like a mixin, or do both in one place.

declaring_traits.rz
trait Damageable:	signal damaged(amount: int)	const HIT_FLASH_TIME := 0.08	func apply_damage(amount: int) -> void	func report_damage(amount: int) -> void:		damaged.emit(amount)
  • Declare a local trait with `trait Name:` inside a script, or make a top-level global trait with `trait_name Name`.
  • A trait may declare constants, variables, signals, enums, and functions.
  • Leave a function body off to make that function a required contract for every class that uses the trait.
  • Write a normal function body when the trait should provide default behavior that gets copied into the using class.

Using traits in classes

Use `uses TraitName` inside a class to pull the trait into that class. Implemented members become part of the class scope, and bodyless functions become requirements the class must satisfy.

using_traits.rz
trait Interactable:	signal interacted(by: Node)	func interact(by: Node) -> void	func announce(by: Node) -> void:		interacted.emit(by)class Door extends Node:	uses Interactable	func interact(by: Node) -> void:		announce(by)		print("Door opened by ", by.name)
  • Write `uses TraitName` near the top of the class body.
  • If the trait declares a bodyless function signature, the class must implement that exact signature.
  • If the trait defines fields, constants, signals, or helper methods, those members are added to the using class.
  • A trait can also declare an `extends` requirement so only classes with a compatible base type may use it.
  • A class can add extra overloads with the same function name as long as required trait signatures are still present.

Overriding trait behavior

A class may override an implemented trait function to customize behavior, but the override still has to honor the trait's declared shape. This gives you default behavior without losing per-class specialization.

trait_overrides.rz
trait Highlightable:	func outline_color() -> Color:		return Color.YELLOWclass Chest:	uses Highlightable	func outline_color() -> Color:		return Color.ORANGE_REDfunc debug_color(target: Chest) -> void:	print(target.outline_color())

Testing and casting with traits

Traits participate in `is` checks and `as` casts, so code can check what an object supports instead of forcing one concrete type.

trait_test_cast.rz
trait Lootable:	func collect() -> voidfunc try_collect(target: Variant) -> void:	if target is Lootable:		var lootable = target as Lootable		lootable.collect()	else:		print("Nothing to collect")
  • Use `value is TraitName` when you need a boolean capability test.
  • Use `value as TraitName` when you want the matching typed value or `null` on failure.
  • The runtime validates trait membership against the script attached to the object, not just the native Godot class.
  • Prefer checking a trait when several unrelated classes can answer the same behavior contract.

Trait-typed variables, collections, and APIs

Traits can be used anywhere you would normally use a type annotation: variables, parameters, returns, signals, arrays, and dictionaries.

trait_typed_apis.rz
trait Command:	func run() -> voidsignal queued(command: Command)var current: Commandvar history: Array[Command] = []var named: Dictionary[String, Command] = {}func submit(command: Command) -> Command:	queued.emit(command)	history.append(command)	named["last"] = command	current = command	return command
  • Annotate variables as `var actor: Moveable` when any compatible implementation is acceptable.
  • Use `Array[TraitName]` and `Dictionary[String, TraitName]` for typed containers of capabilities.
  • Signals, parameters, and return types can all name a trait directly.
  • Container checks validate each stored element against the trait, not just the array or dictionary shell.
  • This is the main way to keep plugin-style or component-style gameplay code typed without introducing a deep inheritance tree.

Section 14

Abstract Class

Use `@abstract` to mark classes that define contracts but cannot be instantiated. Abstract methods (`@abstract func`) require subclass implementation.

Declaring abstract classes

abstract_class.rz
@abstract class Ability:	@abstract func activate(target: Node) -> void	func describe() -> String:		return "Shared ability contract"class Fireball extends Ability:	func activate(target: Node) -> void:		print("Fireball hits ", target.name)func cast(target: Node) -> void:	var spell := Fireball.new()	spell.activate(target)
  • Write `@abstract class Name:` to mark a class as non-instantiable.
  • Abstract classes can still hold implemented helper methods, shared state, and inherited base types.
  • Calling `.new()` or using a builder constructor on an abstract class is rejected by the analyzer.
  • Use abstract bases when several concrete classes need to share one typed interface.

Defining abstract methods

Use `@abstract func` to declare required method signatures without a body. Subclasses must implement them, or remain abstract too.

abstract_methods.rz
@abstract class Enemy:	@abstract func attack(target: Node) -> void@abstract class Boss extends Enemy:	func taunt() -> void:		print("You cannot win.")class SlimeBoss extends Boss:	func attack(target: Node) -> void:		print("Slime bash -> ", target.name)
  • Declare an abstract method as `@abstract func name(args) -> Type` and leave off the body.
  • If a class still contains unimplemented abstract methods, that class must also be marked `@abstract`.
  • Abstract methods cannot be `static` and cannot define a function body.
  • A subclass may inherit from another abstract class and become concrete only after implementing every required method.

Section 15

Constructors

Ruzta adds `builder constructor` for inline node tree construction with control flow and automatic `add_child()` calls.

Instantiating with `new()`

constructor_new.rz
class Projectile:	var speed := 300.0func test() -> void:	var projectile := Projectile.new()	var marker := Node2D.new()	print(projectile.speed)	marker.free()
  • Call `TypeName.new()` for native engine classes such as `Node`, `Label`, or `Timer`, and `MyScript.new()` for script classes.

Using builder constructor

Builder constructor constructs the instance first, then runs a builder body inside `{ ... }` for setup, control flow, and optional nested child builders.

builder_constructor.rz
func make_pause_menu(show_debug: bool, entries: Array[String]) -> VBoxContainer:	return VBoxContainer {		name = "PauseMenu"		alignment = BoxContainer.ALIGNMENT_CENTER		if show_debug:			name = "PauseMenuDebug"		else:			name = "PauseMenuRelease"		while get_child_count() < entries.size():			if get_child_count() >= 4:				break			Button {				text = entries[get_child_count()]			}		for label in ["Paused", "", "Resume"]:			if label == "":				continue			Button {				text = label			}	}
  • Use `TypePath { ... }` when the target can be constructed without arguments.
  • Use `TypePath(arg1, arg2) { ... }` when `_init()` needs constructor arguments before the builder body runs.
  • Builder bodies support receiver-relative assignments/calls, nested builders, and control flow (`if` / `for` / `while`, including `break` and `continue`).
  • Inside builder control expressions, bare names resolve receiver-first; if no receiver member matches, normal scope/global lookup is used.
  • `await` and `return` are not allowed inside builder constructors.
  • The closing `}` only needs to appear as the next structural terminator; it does not need to align with prior indentation.
  • Nested builder_constructor children call `add_child(child)` automatically when the current receiver exposes a compatible `add_child()` parameter.

Releasing objects with `free()`

When you manually construct engine objects and they are not being kept alive by scene ownership or reference counting, release them explicitly with `free()`.

object_free.rz
func test() -> void:	var node := Node.new()	node.name = "Temporary"	node.free()
  • Use `free()` on objects such as `Node` instances you created yourself and no longer need.
  • Do not keep using an object after calling `free()` on it; the instance is gone immediately.
  • Ref-counted types usually die when references disappear, but `Object` and scene objects often need explicit lifetime handling.

Section 16

Generics

Use `@generic(T)` on classes and functions for type parameterization. Supports constraints (`@generic(T: Node)`), multiple type parameters, and inference from call sites.

Generic classes, functions, and constraints

generics.rz
@generic(T)class Box:	var value: T	func _init(v: T):		value = v	func get_value() -> T:		return value@generic(T)func identity(value: T) -> T:	return value@generic(K,V)func pick(values: Dictionary[K, V], key: K) -> V:	return values[key]@generic(T)func dup(value: T) -> (T, T):	return (value, value)@generic(T)func child_as(parent: Node, index: int) -> T:	return parent.get_child(index) as Tfunc test() -> void:	var int_box := Box<int>.new(10)	var str_box := Box<String>.new("hello")	print(int_box.get_value(), str_box.get_value())	print(identity(10), identity<int>(5), identity<String>("ok"))	var stats: Dictionary[String, int] = { "hp": 9 }	print(pick<String, int>(stats, "hp"))	var pair: (int, int) = dup<int>(2)	print(pair.0, pair.1)
  • Use `@generic(T)` on classes and call them as `Box<int>.new(...)` or `Box<String>.new(...)`.
  • Use `@generic(T)` on functions and call with explicit type arguments such as `identity<int>(10)`, or let the call infer them with `identity(10)` when the arguments provide enough information.
  • Explicit type arguments always take priority over inference, so `identity<int>(10)` stays deterministic even when the call site could be inferred.
  • Use multiple type parameters: `@generic(K, V)` then `Dictionary[K, V]` and calls like `pick<String, int>(...)`.
  • Use constraints with `@generic(T: Node)` to require a base class or subtype.
  • Generic parameters are valid inside aggregate annotations like `Array[T]`, `Dictionary[K, V]`, and tuple returns like `(T, T)`.

Section 17

Annotations

Annotations start with `@` and modify the next declaration. Common ones: `@export`, `@onready`, `@tool`. Ruzta adds scoped annotation blocks, `@feature`/`@feature_any` for platform gating, and `@warning_ignore`.

Common annotations you will use first

annotations.rz
@toolextends Node2D@export_group("Movement")@export var speed := 240.0@export var jump_force := 420.0@onready var sprite = $Sprite2D
  • Use `@export` to make a property editable in the Inspector.
  • Use `@export_group`, `@export_subgroup`, or `@export_category` to keep Inspector-heavy scripts readable.
  • Use `@onready` for node lookups or values that should initialize after the node enters the scene tree.
  • Use `@tool` when a script should also run in the editor.

Scoped annotation blocks

Some annotations can open an indented block with `:` and apply to every compatible declaration or statement inside. This is useful when several consecutive lines should share the same export, warning, or feature behavior.

scoped_annotations.rz
extends Node@export:	var grouped_a := 1	var grouped_b := 2func test() -> void:	@warning_ignore("unused_variable"):		var cached = grouped_a		print(cached)
  • Put `:` after the annotation, then start the affected block on the next line.
  • Use scoped blocks to avoid repeating the same annotation on several consecutive declarations.
  • Class-level scoped blocks work well for grouped `@export` members, while statement-level blocks are useful for warnings or feature-gated code regions.
  • Only annotations that support the target inside the block are applied; incompatible targets still raise normal errors.

@feature and @feature_any for gated declarations and blocks

`@feature(...)` and `@feature_any(...)` are the annotation forms of `OS.has_feature(...)` checks. They can gate a single declaration or an entire scoped block: `@feature(...)` combines names with logical AND, while `@feature_any(...)` combines them with logical OR.

feature_annotation.rz
@feature("windows")var windows_only = "win"@feature_any("web_android", "web_ios")var mobile_web = truefunc test() -> void:	@feature("windows", "editor"):		print(windows_only)	@feature("editor")	@feature_any("windows", "linux"):		print("Editor helper for desktop platforms.")	@feature("web"):		print("Only present when the web feature exists.")
  • Use string literals only: `@feature("windows")`, `@feature("windows", "editor")`, or `@feature_any("web_android", "web_ios")`.
  • Apply either annotation directly to variables, constants, functions, classes, traits, enums, signals, statements, or to a scoped annotation block.
  • Code can only use a gated declaration from the same or a stricter feature context, so an ungated access to a gated variable is an analysis error.
  • Inactive feature blocks are removed before runtime code generation; editor builds still analyze them so mistakes inside false blocks are caught early.

Warnings and advanced annotations

Ruzta also supports annotations that affect warnings or class behavior. These are useful once you are tuning editor feedback or expressing more specialized intent.

warning_ignore.rz
@warning_ignore("unused_parameter")func _process(_delta: float) -> void:	pass
  • Use `@warning_ignore(...)` when a specific warning is noisy and you are intentionally keeping the code as written.
  • Use warning ignores narrowly; they should document a conscious exception, not hide sloppy code.
  • Other annotations such as `@abstract` or `@static_unload` are more specialized and should be introduced only when their behavior is needed.

Section 18

Style Reference

Follow GDScript conventions: `snake_case` for variables/functions, `PascalCase` for classes, `CONSTANT_CASE` for constants.

Formatting and naming

Ruzta reads best when it follows the same clear, conservative style that Godot recommends for GDScript: strong naming, one clear statement per line, and indentation that makes control flow obvious.

style.rz
const MAX_SPEED = 400.0@export var move_speed := 220.0func apply_damage(amount: int) -> void:	if amount <= 0:		return	health -= amount
  • Use tabs for indentation and keep block depth visually clean.
  • Use `snake_case` for variables and functions.
  • Use `PascalCase` for classes and `CONSTANT_CASE` for constants.
  • Use traits to keep code DRY (Don't Repeat Yourself)
  • Use Tuple to hold data.

Section 19

Builtin Functions

Ruzta has access to Godot's standard builtins: `print`, `len`, `str`, `typeof`, `range`, `preload`, `load`, and more from `@GlobalScope`.

Everyday builtins

Ruzta scripts have access to the usual Godot globals plus script-level helpers. In practice, that means you already have a large toolkit available before writing any utility class of your own.

builtins_everyday.rz
func _ready() -> void:	assert(len("ruzta") == 5)	print(typeof(3.5))	print(char(65))	print(ord("A"))
  • Use `print`, `prints`, and `printerr` for output.
  • Use `len`, `str`, `int`, `float`, and `typeof` for basic conversions and inspection.
  • Use `range` constantly in loops and `assert` when you want a development-time correctness check.
  • Use `char` and `ord` when you need character/code point conversions.

Loading, debugging, and runtime helpers

A second group of builtins covers resource loading, stack inspection, and dynamic type checks. These are the helpers you reach for when wiring projects together or debugging script behavior.

builtins_runtime.rz
const HUD_SCENE = preload("res://ui/hud.tscn")func spawn_hud(scene_path: String) -> void:	var hud_scene = load(scene_path)	print_debug(hud_scene)	print(is_instance_of(HUD_SCENE, TYPE_OBJECT))
  • Use `preload` for constant asset references known at parse time and `load` for dynamic paths chosen at runtime.
  • Use `print_debug`, `print_stack`, and `get_stack` when a regular `print` is not enough.
  • Use `is_instance_of` when the type you want to compare against is itself dynamic.
  • Remember that many engine-wide constants and helpers come from `@GlobalScope`, not only the script helper set.