Ruzta documentation
(roo.stuh)
Setup
Download and enable Ruzta
Ruzta scripts live in `.rz` files, but the language itself is delivered as a Godot GDExtension. The first setup job is getting the extension bundle into a project where Godot can load it.
extends Nodefunc _ready() -> void: print("Hello from Ruzta")- Download the current build archive, then unzip it before opening the project in Godot.
- Keep `ruzta.gdextension` together with the shipped `lib/` binaries so the manifest can resolve the correct platform library.
- A Ruzta source file uses the `.rz` extension; script resources can also be exported as `.rzc` token streams later in a pipeline.
- If Godot does not recognize the language immediately, reopen the project so the extension reloads cleanly.
- Treat the archive like an addon bundle: copy the whole runtime payload, not just the script files.
First `.rz` script
If you already know Godot scenes, the quickest proof that your setup works is to attach a tiny `.rz` script to a node and print 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.
Learn more
Ruzta is intentionally close to GDScript, so the fastest way to level up is to combine this site with the official Godot scripting references and a couple of practice-oriented guides.
- Use the official GDScript docs as the default syntax reference when you need exact statement or type behavior.
- 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.
Ruzta vs GDScript
What changes in practice
The biggest differences are packaging and cadence, not the 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.
- When you share a project, you need the Ruzta extension bundle available alongside the project, not only the scripts.
- 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`.
- Named argument calls use `fn(param: value)` syntax rather than GDScript's `fn(value, param=value)`.
- 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.
Variables and Constants
Declaring values
Use `var` for mutable state and `const` for values that must not change. Class variables live on the script object, while local variables live inside a function body.
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.
- Constants are best for shared configuration, cached preloads, and enum-like numeric values that should not be reassigned.
- Local declarations work the same way inside functions, loops, and match branches.
- Use clear defaults so the editor and your teammates can read intent before the scene ever runs.
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_count- Instance variables are unique per object; static variables are shared by all instances of that script class.
- Use static state sparingly for counters, registries, or caches that really are global to the script type.
- A local variable shadows an outer name in the usual way, so keep names distinct when possible.
Primitive Types
Core value types
Ruzta works with the same core Godot value model you already know: booleans, integers, floats, strings, string names, node paths, engine math types, objects, and `Variant` when something may be dynamic.
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.
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]`.
- Prefer explicit casts in API boundaries so the next reader can see when narrowing is intentional.
Aggregate Types
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.
- Many builtins and engine APIs accept arrays, so they are the default collection type for batched values.
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.
- Literal dictionaries are concise, so they work well for local tables and compact state maps.
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`).
Functions, Lambdas, and Setters/Getters
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 > 0- Use return types on public helpers and shared gameplay APIs so call sites are easier to understand.
- Default values are good for optional tuning parameters and convenience overloads.
- Scene callbacks are regular functions, so everything you learn here applies to `_ready()`, `_process()`, `_input()`, and friends.
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.
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.
- Generic overload candidates can infer their type arguments from the call site when no explicit type arguments are provided.
- 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.
Enum
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 and 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(_)`.
- Matches against enum-typed values are exhaustive by default; add every case or use `_:` to opt out intentionally.
- 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.
Control Flow
Conditionals
Use `if`, `elif`, and `else` for straightforward branching. Ruzta also keeps the 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.
- Keep conditions readable; extract helper functions instead of stacking several unclear expressions together.
Pattern matching
Use `match` when the branching logic is state-driven or pattern-shaped. 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.
Operators
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.
Printing and String Formatting
Console output and formatted strings
Ruzta keeps the common Godot output helpers such as `print`, `prints`, and `printerr`, and it supports percent-style string formatting for compact status text and debug lines.
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.
Signals and Concurrency
Signals
Signals decouple systems cleanly. Declare them with `signal`, connect listeners with `connect()`, and emit them from the producer when something interesting happens.
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 first-class `Signal` value has a known signature, the editor validates `emit(...)` arguments and `connect(...)` callables against that signal shape. This keeps signal contracts visible at the use site instead of only at the declaration.
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`.
- A signal with no parameters resumes with `null`, one parameter resumes with that value, and multiple parameters resume with an array payload.
- Timers and other signal-emitting engine APIs compose naturally with `await`.
- Mark code structure clearly after an `await`, because execution resumes later, not immediately.
Class and Global
Unnamed scripts and `class_name`
Every `.rz` file defines a script class, even if it does not declare `class_name`. Adding `class_name` simply 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.
- Keep singleton APIs narrow and explicit so they do not turn into a hidden dumping ground.
- A singleton name behaves like a global entry point, but the underlying implementation is still just a script or node you own.
Traits
Declaring traits
Traits let you describe shared behavior without forcing one inheritance chain. 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.
- Use traits when one capability should apply across unrelated classes without turning your class hierarchy into a ladder.
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.
- Traits are resolved before the rest of the class body is checked, so typed access to trait members works naturally from the class.
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())- A class override matches at signature level: the same parameter list and return shape replaces the trait-provided implementation.
- If the class overload uses the same name but a different signature, it is treated as an additional overload, not a replacement.
- Use trait implementations for shared defaults, then override only where concrete classes truly differ.
- Traits do not override members from other traits when one trait uses another; conflicts are rejected instead.
- You also cannot call a trait directly as if it were an instance. Call through a class that uses it.
Testing and casting with traits
Traits participate in `is` checks and `as` casts, so capability-style code can ask what an object supports instead of forcing one concrete type. This is useful for gameplay interactions, targeting, and shared service APIs.
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. This keeps capability-based code strongly typed without hard-coding one concrete implementation.
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.
Global traits and exported trait references
Global traits give one capability a project-wide name. They behave like type declarations and reusable member bundles, not like instantiable scripts. Node-compatible trait types can also drive editor-facing `@export` annotations.
trait_name ActorTraitextends Nodefunc actor_name() -> String: return nameextends Node@export var leader: ActorTrait@export var squad: Array[ActorTrait]@export var lookup: Dictionary[String, ActorTrait]- Declare a global trait with `trait_name TraitName` at the top of a `.rz` file.
- A global trait is not something you instantiate with `.new()` or attach directly to a node.
- Use a trait that extends `Node` when you want trait-typed exports for nodes and node collections.
- Trait-typed exports can also be used inside `Array[...]` and `Dictionary[..., ...]` type annotations so editor metadata stays informative.
- Use global traits for project-wide contracts such as combat capabilities, quest hooks, AI commands, or service interfaces.
Abstract Class
Declaring abstract classes
Use `@abstract` on a class when it defines shared behavior and a contract for subclasses, but should not be instantiated directly.
@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.
Constructors
Instantiating with `new()`
Use `.new()` to create instances from native classes, script classes, inner classes, and preloaded script resources. This remains the standard constructor form in Ruzta.
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`.
- Call `MyScript.new()` for top-level script classes and `Outer.Inner.new()` for inner classes.
- Use constructor arguments exactly where `_init()` expects them; if `_init()` requires values, `.new()` must supply them.
Using `builder constructor`
`builder constructor` constructs the instance first, then runs a constrained builder body inside `{ ... }` for receiver-relative 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.
Generics
Generic classes, functions, and constraints
Ruzta supports `@generic(...)` on classes and functions. You can specialize with explicit type arguments, infer function type arguments from call sites when possible, and reuse those generic parameters inside `Array[...]`, `Dictionary[..., ...]`, and tuple types.
@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: Node)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)`.
Annotations
Common annotations you will use first
Annotations start with `@` and modify the next declaration. In day-to-day gameplay scripts, the most common ones are `@export`, `@onready`, and grouping annotations for the Inspector.
@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.
Style Reference
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.
- Prefer short functions with explicit names over very clever multi-purpose ones.
- Add types where the API matters most, especially on exported members and public helpers.
- Keep node lookups near `@onready` declarations instead of scattering `$Node` calls everywhere.
Builtin Functions
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.