A 2D game
2D in Balaur is not a separate engine: it is a second set of components over
the same scene tree. This tutorial walks through the concepts using
examples/angrynerds, a small slingshot game that ships with the repository:
cargo run -p balaur_cli --features window -- run examples/angrynerds
cargo run -p balaur_cli --features window -- edit examples/angrynerds
2D nodes
A 2D node uses the regular Transform: x/y translate, the z-axis rotation
spins it, x/y scale. The 2D components are shape2d (rendering), body2d and
collider2d (physics), and widget (HUD elements):
[[nodes]]
name = "Ground"
position = [0, -0.5, 0]
body2d = "static"
shape2d = { kind = "rect", half_extents = [14, 0.5] }
collider2d = { kind = "rect", half_extents = [14, 0.5], friction = 0.9 }
color = [0.45, 0.63, 0.35]
[[nodes]]
name = "Score"
[nodes.widget]
kind = "label"
text = "Score 0"
anchor = "top_left"
x = 20
y = 16
font_size = 22
text_color = "#2b2b2b"
Shapes are rect and circle. Widgets are label, button, and panel,
anchored to a screen corner or the center; they live as scene-tree nodes and
render through the widget layer.
The camera
The 2D camera is orthographic with pan and zoom. Zoom is expressed in logical pixels per world unit:
render.set_camera_2d(0, 3, 52) -- center x, center y, zoom
local cx, cy, zoom = render.camera_2d()
render.mouse_world_2d() converts the cursor to world coordinates for
picking, and render.draw_line_2d(...) draws debug or aiming overlays — the
example uses both for the slingshot band.
2D physics
body2d and collider2d run in a rapier2d world with the same determinism
guarantees as 3D: the enhanced-determinism build, a fixed 60 Hz
accumulator, and ordered collections. The physics2d module drives it from
scripts:
physics2d.set_gravity(0, -9.81)
physics2d.add_body(node, physics2d.BODY_DYNAMIC)
physics2d.add_collider(node, { kind = physics2d.SHAPE_CIRCLE, radius = 0.4 })
physics2d.apply_impulse(node, dx * POWER, dy * POWER)
physics2d.max_contact_impulse(node) returns the strongest impact a body took
this frame — the example uses it to decide when a pig is popped.
Components from scripts
Scripts read and write the same components the scene file declares. Updating the score label is a component write:
local w = node:get_component("widget")
w.text = "Score " .. self.score
node:set_component("widget", w)
Removing a node's 2D components (remove_component("body2d"), ...) takes it
out of both the renderer and the physics world without freeing it, which is
how the example implements restarts: capture every node's initial components
in init, re-apply them on R.
The editor
The editor detects a 2D scene from its components and switches automatically: 2D grid, pan/zoom camera, a 2D selection gizmo, click-picking in world space, and 2D collider overlays. Because the whole level is scene data, it is editable in the editor without touching the script.