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å denne siden
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.