raylib Guide
Install Iron, open a window, then ship a 2D game, a 3D scene, and a post-FX shader pipeline.
All code in this guide is lifted verbatim from working files under examples/. Copy-paste works out of the box with the current Iron release.
Overview
The Iron standard library binds raylib 6.0. Every 2D primitive, 3D camera, texture, font, sound, and shader from upstream raylib is callable as idiomatic Iron (Draw.rectangle(...), Texture.load(...), Camera3D(...)). Both native (iron build) and web (iron build --target=web) compile against the same binding.
The binding ships under 14 categories: Types, Enums, Window, Input, 2D Drawing, Collision, Textures, Text, Audio, 3D Drawing, Models, Shaders, Math, Files. This guide walks the 2D / 3D / shader path through three real example files. For per-function reference, jump to the API reference.
Install
Install Iron via the one-line installer (see the Install page for full platform details):
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/victorl2/iron-lang/main/scripts/install.sh | sh
Then confirm the raylib binding ships with the stdlib by creating hello.iron:
import raylib
func main() {
Window.init(640, 480, "hello")
Window.close()
}
Run with iron run hello.iron — a window flashes open and closes cleanly. If it exits 0, the binding is wired and you're ready to go.
Your first window
A raylib program is always the same shape: initialize a window, set a target frame rate, loop until the user closes the window, tear down. Here is the minimal skeleton adapted from examples/pong/pong.iron:
import raylib
func main() {
Window.init(800, 600, "first window")
Window.set_target_fps(60)
while not Window.should_close() {
Draw.begin()
Draw.clear(BLACK)
Draw.text("Hello, Iron + raylib", Int32(200), Int32(280), Int32(24), WHITE)
Draw.end()
}
Window.close()
}
Breaking it down, line by line:
Window.init(w, h, title)— opens the window;raylibhandles platform specifics.Window.set_target_fps(60)— caps the main loop at 60 FPS (delta time is handled internally).while not Window.should_close()— loop until the user closes the window or presses ESC. Iron usesnot, not!.Draw.begin()/Draw.end()— bracket every frame's render commands.Draw.clear(BLACK)— wipe the framebuffer.BLACK,WHITE,RED, and the other built-in color constants come from the binding.Draw.text(str, x, y, size, color)— draws text using the default font. Every integer argument is explicitly boxed (Int32(200)) because raylib's C ABI takesint, and Iron is strict about integer widths at foreign boundaries.Window.close()— releases the GL context, audio device, and window handle.
Pong — a complete 2D game
The examples/pong/pong.iron file exercises the whole 2D surface: state machine, paddles, ball, collision, and audio. It is the best small reference game in the repo when you want to see how the pieces fit together. We'll walk through the structural pieces here.
Source: examples/pong/pong.iron
Setup & state
Pong's top of file sets up the state machine (title / playing / game over), color constants, input bindings, and starting positions:
import raylib
-- Game state enum for the title -> play -> game over -> restart state machine
enum GameState {
TITLE = 0,
PLAYING = 1,
GAME_OVER = 2,
}
func main() {
val title: String = "Iron Pong"
val bg: Color = BLACK
val fg: Color = WHITE
val accent: Color = RED
val divider: Color = DARKGRAY
val ball_origin: Vector2 = Vector2(Float32(400.0), Float32(300.0))
val start_key: KeyboardKey = KeyboardKey.SPACE
val left_up: KeyboardKey = KeyboardKey.W
val left_down: KeyboardKey = KeyboardKey.S
val right_up: KeyboardKey = KeyboardKey.UP
val right_down: KeyboardKey = KeyboardKey.DOWN
val initial_state: GameState = GameState.TITLE
Window.init(800, 600, title)
Window.set_target_fps(60)
-- ...
}
Pong formats its scores with to_string() and pad_left(3, " ") so the scoreboard stays fixed-width as scores grow:
val left_str = left_score.to_string().pad_left(3, " ")
val right_str = right_score.to_string().pad_left(3, " ")
Draw.text(left_str, Int32(200), Int32(20), Int32(40), WHITE)
Draw.text(right_str, Int32(600), Int32(20), Int32(40), WHITE)
The ball
The ball's origin is captured in a Vector2; its velocity is updated each frame and its position is drawn as a filled rectangle. The plain Vector2 constructor takes Float32 components — raylib is float-first at the ABI boundary:
val ball_origin: Vector2 = Vector2(Float32(400.0), Float32(300.0))
-- Inside the Draw.begin() / Draw.end() block:
Draw.rectangle(0, 250, 10, 100, fg)
Draw.line(400, 0, 400, 600, divider)
For the full ball update loop (velocity integration + bounds reflection) see the pong source on GitHub; the key binding calls are Draw.rectangle for the ball body, Draw.line for the divider, and Rectangle.collides for edge/paddle overlap checks.
Paddles & input
Player input is polled with Keyboard.is_down (held) and Keyboard.is_pressed (edge). Every Iron raylib program uses the same KeyboardKey enum exposed by the binding:
val _kb_start: Bool = Keyboard.is_pressed(start_key)
val _kb_left_up: Bool = Keyboard.is_down(left_up)
val _kb_left_down: Bool = Keyboard.is_down(left_down)
val _kb_right_up: Bool = Keyboard.is_down(right_up)
val _kb_right_down: Bool = Keyboard.is_down(right_down)
In the real game loop these drive the paddle y positions and the SPACE press transitions GameState.TITLE -> PLAYING. Paddles themselves are drawn with Draw.rectangle(x, y, w, h, color) — the same primitive that renders the ball.
Collision
Pong uses axis-aligned rectangle intersection to bounce the ball off the paddles and the top/bottom walls. The binding exposes this as a receiver method on Rectangle:
val paddle = Rectangle(Float32(0), Float32(250), Float32(10), Float32(100))
val ball = Rectangle(Float32(395), Float32(295), Float32(10), Float32(10))
if Rectangle.collides(paddle, ball) {
-- reverse ball velocity, play bounce sound
}
See the Collision reference for the full set (rect/rect, rect/circle, circle/circle, point-in-rect, 3D sphere/box/ray).
Audio
Audio in raylib is a two-step dance: initialize the audio device, then load and play sounds. Pong wires up a bounce effect:
Audio.init()
val bounce: Sound = Sound.load("tests/assets/bounce.wav")
-- In the collision handler:
if Sound.is_valid(bounce) {
val _r = Sound.play(bounce)
}
-- Teardown: unload the sound, then close the audio device
Sound.unload(bounce)
Audio.close()
The Sound.is_valid guard is the common pattern for "asset failed to load at runtime (missing file, wrong format, headless CI)" — a missing bounce.wav should not crash the game. Binding the result of Sound.play keeps the call explicit while still treating playback as fire-and-forget. For streaming / music / mixed-pool audio, see the Audio reference.
Full source: pong.iron on GitHub
Rotating cube — your first 3D scene
The file examples/rotating_cube/rotating_cube.iron is 62 lines end-to-end — window + orbital camera + cube + wireframe + grid + main loop. It's the Iron version of raylib's "hello 3D" example and it's the cleanest template for any 3D program.
Source: examples/rotating_cube/rotating_cube.iron
Camera3D
A Camera3D is constructed from position, target, up vector, FOV, and projection mode. Note the var keyword — raylib's orbital update rebinds the camera each frame, so it needs to be mutable:
var cam = Camera3D(
Vector3(Float32(10.0), Float32(10.0), Float32(10.0)),
Vector3(Float32(0.0), Float32(0.0), Float32(0.0)),
Vector3(Float32(0.0), Float32(1.0), Float32(0.0)),
Float32(45.0),
CameraProjection.PERSPECTIVE
)
CameraProjection.PERSPECTIVE and CameraProjection.ORTHOGRAPHIC can be passed directly to Camera3D(...).
The 3D render loop
3D drawing is bracketed by Draw.begin_mode_3d(cam) / Draw.end_mode_3d(), nested inside the normal Draw.begin() / Draw.end() frame. Between them, every Draw.* call renders in 3D space:
val origin = Vector3(Float32(0.0), Float32(0.0), Float32(0.0))
val size = Float32(2.0)
while not Window.should_close() {
cam = Camera3D.update(cam, CameraMode.ORBITAL)
Draw.begin()
Draw.clear(SKYBLUE)
Draw.begin_mode_3d(cam)
Draw.cube(origin, size, size, size, RED)
Draw.cube_wires(origin, size, size, size, BLACK)
Draw.grid(Int32(10), Float32(1.0))
Draw.end_mode_3d()
Draw.end()
}
Things to notice:
Camera3D.update(cam, CameraMode.ORBITAL)— raylib supplies the rotation; no explicit angle math needed.Draw.cube/Draw.cube_wires— filled plus wireframe for that classic look.Draw.grid(slices, spacing)— the ground plane. Takes noColor(raylib draws it in a fixed gray).
Build & run
iron build examples/rotating_cube/rotating_cube.iron
./rotating_cube
A window opens at 800x600 with a red cube orbiting over a gray grid under a sky-blue backdrop. ESC or window-close exits cleanly. For the full 3D surface — cylinders, capsules, planes, rays, more camera modes — see the 3D Drawing reference.
Post-FX — the shader pipeline
The file examples/post_fx/post_fx.iron demonstrates the full multi-pass pipeline: render a 3D scene to an offscreen texture, then draw that texture through a fragment shader. It's the blueprint for screen-space effects, bloom, color grading, anything post-processing. Press SPACE at runtime to toggle between grayscale and invert shaders.
Source: examples/post_fx/post_fx.iron · Shaders: tests/assets/shaders/
The pipeline
Two Shader resources loaded at startup with Shader.load, then per frame: begin texture mode -> draw 3D -> end texture mode -> begin shader mode -> draw the texture full-screen -> end shader mode:
-- Offscreen render target for 3D scene
val rt = RenderTexture.load(Int32(800), Int32(600))
-- Empty VS string uses raylib's default vertex stage (full-screen quad)
val fx_grayscale = Shader.load("", "tests/assets/shaders/grayscale.fs")
val fx_invert = Shader.load("", "tests/assets/shaders/invert.fs")
while not Window.should_close() {
-- Pass 1: render 3D scene into offscreen RenderTexture
Draw.begin_texture_mode(rt)
Draw.clear(RAYWHITE)
Draw.begin_mode_3d(cam)
Model.draw(model, origin, Float32(1.0), WHITE)
Draw.end_mode_3d()
Draw.end_texture_mode()
-- Pass 2: draw RenderTexture through selected post-FX shader
Draw.begin()
Draw.clear(BLACK)
Draw.begin_shader_mode(active_shader)
-- Y-flip via negative-height Rectangle — OpenGL RT origin is bottom-left
Texture.draw_rec(
rt.texture,
Rectangle(Float32(0.0), Float32(0.0), Float32(800.0), Float32(-600.0)),
Vector2(Float32(0.0), Float32(0.0)),
WHITE
)
Draw.end_shader_mode()
Draw.end()
}
The Y-flip via negative-height Rectangle is a raylib idiom: OpenGL RenderTexture origins are bottom-left, but screen origins are top-left. Drawing with a negative height mirrors the texture on output.
Uniforms
Shader uniforms in Iron are resolved by name once at load time, then written per frame. The invert shader takes a u_intensity float; we pack it as an IEEE-754 little-endian byte buffer and call Shader.set_value every frame:
-- Resolve invert.fs's u_intensity uniform once at load
val loc_intensity = Shader.get_location(fx_invert, "u_intensity")
-- Float32(1.0) IEEE-754 LE = 0x3F800000 = [0x00, 0x00, 0x80, 0x3F]
val intensity_bytes: [UInt8] = [UInt8(0), UInt8(0), UInt8(128), UInt8(63)]
while not Window.should_close() {
-- SPACE key toggles between shaders
if Keyboard.is_pressed(KeyboardKey.SPACE) {
if current_fx == Int32(0) {
current_fx = Int32(1)
} else {
current_fx = Int32(0)
}
}
-- raylib ignores the write if loc_intensity == -1 (uniform not found)
if loc_intensity >= Int32(0) {
Shader.set_value(fx_invert, loc_intensity, intensity_bytes, ShaderUniformDataType.FLOAT)
}
-- ... render passes ...
}
For matrix uniforms, texture samplers, and the full ShaderUniformDataType enum, see the Shader reference.
API Reference
When you know what you need but not the exact signature, jump into the reference. Start with Types for the core value types (Vector2, Vector3, Color, Rectangle, Camera3D), then Enums, then the category you need. Every entry lists signature, one-line description, a 3-5 line example, and 3 cross-links (raylib.h upstream, Iron source line, test usage).
Examples gallery
Five example programs under examples/ exercise every category of the binding. Read their source for concrete usage patterns:
pong— 2D game: window, input, draw, collision, audio.rotating_cube— 3D scene: Camera3D, 3D drawing, orbital camera mode.model_viewer— 3D model loading with procedural fallback.post_fx— shader pipeline with RenderTexture and interactive uniforms.raylib_showcase— single-file showcase covering the full binding.
The examples gallery adds screenshot cards for each of these programs.