Coming from Godot
A Godot to Balaur mapping, one line each: project.godot to project.toml, .tscn to TOML, node types to components, GDScript to Rune, export to one binary.
På den här sidan
Godot to Balaur, one row per file, node and call; the manual and the reference have the rest.
Files
| Godot | Balaur |
|---|---|
project.godot |
project.toml: name, main_scene, input actions |
scene.tscn |
scenes/scene.toml: a flat [[nodes]] list, parent by id |
script.gd |
scripts/script.rn: Rune |
.tres resources |
[[assets]] in TOML: mesh, material, animation_library, tileset, … (assets) |
.gdshader |
.wesl in a material asset (shaders) |
| Export presets, templates | balaur export my-game --target linux-x64: one fused binary (shipping) |
Nodes
No class hierarchy. A node is whatever its components make it. A collider sits on the same node as its body.
| Godot | Balaur |
|---|---|
Node2D, Node3D |
A node with a transform: position, rotation, scale. A node without one sits at its parent |
RigidBody2D, StaticBody2D, AnimatableBody2D |
body2d = { kind = "dynamic" } (or static, kinematic); body3d in 3D |
CollisionShape2D |
collider2d = { kind = "rectangle", size = [1.0, 1.0] } |
CharacterBody2D, CharacterBody3D |
character2d, character3d |
PinJoint2D, HingeJoint3D, … |
joint2d, joint3d |
VehicleBody3D, VehicleWheel3D |
vehicle3d, wheel3d |
Sprite2D |
sprite |
TileMapLayer |
tilemap over a tileset asset |
GPUParticles2D, CPUParticles2D |
particles2d |
GPUParticles3D, CPUParticles3D |
particles3d |
MeshInstance3D, CSGBox3D, … |
mesh for a model; shape3d for a primitive; boolean3d for CSG |
Polygon2D |
polygon |
Camera2D |
camera2d |
Camera3D |
camera3d |
PointLight2D, LightOccluder2D |
light2d, occluder2d |
AudioStreamPlayer, AudioListener3D |
sound, listener |
Skeleton2D / Bone2D, Skeleton3D |
Nodes with bone2d / bone3d |
SkeletonIK, LookAtModifier, SkeletonModification2DFABRIK / CCDIK / Jiggle |
modifier2d and modifier3d: look_at, two_bone_ik, fabrik, ccdik, jiggle |
BoneMap, SkeletonProfile |
A bone_map asset and animation.play(node, clip, { retarget }) |
PhysicalBone2D, Create Physical Skeleton |
physics2d.ragdoll(node), and a ragdoll component whose influence mixes the clip with the simulation |
AnimationPlayer |
animation: a clip library and an autoplay |
Label, Button, Panel |
widget with kind = "label" / "button" / "panel" |
Control layouts, editor plugins |
The ui module: immediate-mode egui in draw_ui (UI) |
Instancing a PackedScene |
instance = "scenes/crate.toml" on a node, with overrides (prefabs) |
Scripts
| Godot | Balaur |
|---|---|
extends Node2D |
Nothing. 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.0 |
pub fn exports() { #{ speed: 2.0 } }, overridden in [nodes.script.props] |
self |
this; the node is this.node |
position, rotation, scale |
this.node.transform.position, .rotation_euler (radians), .scale; transform.global_position() in world space |
$Path/To/Node, get_node("..") |
this.node.get_node("Path/To/Node"), "../Sibling" |
queue_free() |
node.queue_free(): deferred to end of frame |
apply_impulse(v) |
this.node.body2d.apply_impulse(x, y) |
signal hit on a node, enemy.hit.connect(_on_hit) |
events::listen(this.node, "hit", enemy), then pub fn on_hit(this, payload) (events) |
hit.emit(payload) from that node |
this.node.emit("hit", payload): delivered at the top of the next frame |
| Connecting to every emitter of a signal | events::listen(this.node, "hit"): no emitter argument |
Engine signals: pressed, animation_finished, request_completed |
Methods by name: on_click, on_animation_finished(name), on_response(r) |
| Calling a method on another node | node.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") |
this.node.animation.play("run") |
$AnimationTree["parameters/playback"].travel("run") |
this.node.state_machine.travel("run") |
create_tween().tween_property(...) |
animation::tween(this.node, #{ steps: [...] }): Godot's easing names |
await get_tree().create_timer(1.0).timeout |
task::wait(...) in an async fn; results land at the start of a tick |
HTTPRequest |
http::request(url) (networking) |
FileAccess, ConfigFile |
fs, settings, save |
randf(), randi() |
random::: seeded, engine-owned, replayable |
print() |
log::info() |
preload, load |
assets::load("path.toml") |
The editor
| Godot | Balaur |
|---|---|
| 2D / 3D / Script / AssetLib tabs | Workspaces: Scene, Script, Animation, Physics, UI (editor) |
| Script reloads on save | Hot reload in milliseconds |
| Debugger tab | Breakpoints in the gutter, frames and locals in a dock |
| Editor plugins in GDScript | The editor is a Balaur project; editing its scripts hot reloads the editor |
| Remote scene tree | Play in editor on the same scene and scripts, every run recorded |
Differences
- In Godot only Godot Rapier Physics's physics server is deterministic. In Balaur the whole tick is: fixed step, a digest per tick, record, replay and rollback (determinism).
- There is no
connect. Events are named:events::emit("hit", p)reaches a subscribed node ason_hit(p)next frame. Engine events work the same way, most with a polling form (input::key_just_pressed,events::emitted("hit")). - There are no editor-only nodes. Everything the editor does is a script call any project can make.
- Balaur is new. Compare and the roadmap list what is missing.
Start
balaur import project.godot --project my-game # convert a Godot project
balaur edit my-game
The importer writes the settings, every scene, the scripts and the theme, and
reports what did not carry. examples/angrynerds is a small 2D game: bodies,
colliders, widgets, one script per node.
Getting started has the rest.