raylib Guide

Install Iron, open a window, then ship a 2D game, a 3D scene, and a post-FX shader pipeline.

Every snippet is real

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:

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.

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.

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.

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
)
Use the enum directly

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:

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.

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:

The examples gallery adds screenshot cards for each of these programs.