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.
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()`.
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.
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.
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.
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
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 var spawned_count := 0var nickname := "unit"func _init() -> void: spawned_count += 1static func get_spawned_count() -> int: return spawned_countSection 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
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.
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.
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.
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.
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.
func heal(amount: int = 1) -> void: health += amountfunc is_alive() -> bool: return health > 0Function 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.
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.
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.
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.
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.
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.
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.
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 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.
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 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.
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.
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.
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`.
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
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()`.
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.
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.
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.
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`.
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.
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.
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.
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 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 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 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 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 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()`
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.
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()`.
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
@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
@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.
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("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("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.
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.
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.
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.