Skip to main content

Coming from Godot

The tree of nodes, the scene files, the instancing and the script-per-node model are the ones you know. What changes, one line each. Row by row, the manual and the reference have the rest.

Files

GodotBalaur
project.godotproject.toml: name, main_scene, input actions
scene.tscnscenes/scene.toml: a flat [[nodes]] list, parent by id
script.gdscripts/script.rn: Rune
.tres resources[[assets]] in TOML: mesh, material, animation_clip, tileset, … (assets)
.gdshader.wesl: WGSL with imports, in a material asset (shaders)
Export presets, templatesbalaur export my-game --target linux-x64: one fused binary (shipping)

Nodes

No class hierarchy. A node is whatever its components make it, and a collider sits on the same node as its body, not on a child.

GodotBalaur
Node2D, Node3DA node. Position, rotation, scale on every one
RigidBody2D, StaticBody2D, AnimatableBody2Dbody2d = "dynamic" / "static" / "kinematic": same in 3D
CollisionShape2Dcollider2d = { kind = "rect", half_extents = [0.5, 0.5] }
CharacterBody2D, CharacterBody3Dcharacter2d, character3d
PinJoint2D, HingeJoint3D, …joint2d, joint3d
VehicleBody3D, VehicleWheel3Dvehicle3d, wheel3d
Sprite2Dsprite
TileMapLayertilemap over a tileset asset
GPUParticles2Dparticles
MeshInstance3D, CSGBox3D, …mesh for a model; shape3d for a primitive; boolean3d for CSG
Polygon2Dpolygon
Camera2D, Camera3Dcamera
PointLight2D, LightOccluder2Dlight2d, occluder2d
AudioStreamPlayer, AudioListener3Dsound, listener
Skeleton2D / Bone2D, Skeleton3DNodes with bone2d / bone3d
SkeletonIK, LookAtModifiermodifier2d: look_at, two_bone_ik
AnimationPlayeranimation: a clip library and an autoplay
Label, Button, Panelwidget with kind = "label" / "button" / "panel"
Control layouts, editor pluginsThe ui module: immediate-mode egui in draw_ui (UI)
Instancing a PackedSceneinstance = "scenes/crate.toml" on a node, with overrides (prefabs)

Scripts

GodotBalaur
extends Node2DNothing. A script is free functions taking this
_ready()pub fn init(this)
_process(delta)pub fn update(this, dt)
_physics_process(delta)pub fn fixed_update(this, dt): fixed 60 Hz, before physics
_exit_tree()pub fn on_free(this)
@export var speed = 2.0pub fn exports() { #{ speed: 2.0 } }, overridden in [nodes.script.props]
selfthis; the node is this.node
$Path/To/Node, get_node("..")this.node.get_node("Path/To/Node"), "../Sibling"
queue_free()node.queue_free(): deferred to end of frame, as in Godot
apply_impulse(v)this.node.body2d.apply_impulse(x, y)
signal hit on a node, enemy.hit.connect(_on_hit)events::subscribe(this.node, "hit", enemy), then pub fn on_hit(this, payload) (events)
hit.emit(payload) from that nodethis.node.emit("hit", payload): delivered at the top of the next frame
Connecting to every emitter of a signalevents::subscribe(this.node, "hit"): omit the node, hear them all
Engine signals: pressed, animation_finished, request_completedMethods by name: on_click, on_animation_finished(name), on_response(r)
Calling a method on another nodenode.call("method", args)
Input.is_action_just_pressed("jump")input::action_just_pressed("jump")
Input.get_axis("left", "right")input::action_value("move_x"): one action, -1 to 1
InputMap in project settings[input.actions] in project.toml (input)
$AnimationPlayer.play("run")animation::play(this.node, "run")
create_tween().tween_property(...)animation::tween_to(this.node, "position", [0.0, 9.0, 0.0], 0.2, "out_back"): Godot's easing names and shapes
await get_tree().create_timer(1.0).timeouttask::wait(...) in an async fn; results land at the start of a tick
HTTPRequesthttp::request(url) (networking)
FileAccess, ConfigFilefs, settings, save
randf(), randi()rng::: seeded, engine-owned, replayable
print()log::info()
preload, loadassets::load("path.toml")

The editor

GodotBalaur
2D / 3D / Script / AssetLib tabsPersonas: Scene, Script, Animate, Physics, Interface (editor)
Script reloads on saveHot reload in milliseconds, state intact; a compile error keeps the old code running
Debugger tabBreakpoints in the gutter, frames and locals in a dock
Editor plugins in GDScriptThe editor is a Balaur project; editing its scripts hot reloads the editor
Remote scene treePlay in editor: the real scripts on the real scene, every run recorded

Not the same

  • Determinism. In Godot only Godot Rapier Physics's server is deterministic. Here the whole tick is: fixed step, a digest per tick, record and replay, rollback (determinism).
  • No connect. Events are named, not callbacks: events::emit("hit", p) reaches a subscribed node as on_hit(p) next frame. Engine events work the same way, most with a polling twin (input::just_pressed, events::emitted("hit")).
  • No editor-only nodes. Everything the editor does is a script call any project can make.
  • Maturity. 0.1, weeks old, no binary yet. Compare and the roadmap say what is missing.

Start

cargo run -p balaur_cli --features window -- edit examples/angrynerds

A 2D game (bodies, colliders, widgets, one script per node) small enough to read in one sitting. Getting started has the rest.