Starting a 2D Game with Odin + raylib
raylib supplies a direct window, input, drawing, and time API. An Odin program turns those calls into the standard input-update-draw game loop.
raylib is a small C library for games, exposed in Odin as vendor:raylib. It does not impose entities, scenes, or a physics model. Each frame, the program polls input, updates its own values, and issues drawing commands. That directness is useful when learning because every visible change has an identifiable source.
Platform Setup
Odin ships the raylib binding, so no separate language package is needed. macOS needs Xcode Command Line Tools (xcode-select --install); Windows needs Visual Studio Build Tools with MSVC and the Windows SDK; Linux needs Clang. Create a directory with this main.odin and run it using odin run ..
package main
import rl "vendor:raylib"
main :: proc() {
rl.InitWindow(800, 450, "Odin + raylib")
defer rl.CloseWindow()
rl.SetTargetFPS(60)
for !rl.WindowShouldClose() {
rl.BeginDrawing()
rl.ClearBackground(rl.RAYWHITE)
rl.DrawText("A window is running", 220, 200, 24, rl.DARKGRAY)
rl.EndDrawing()
}
}
InitWindow acquires the native window; defer ensures CloseWindow runs when main ends. WindowShouldClose becomes true when the close control or the platform quit input is received. BeginDrawing and EndDrawing delimit one image. raylib is immediate mode: every frame begins with a blank back buffer and redraws the whole scene.
Input, Update, Draw
The durable structure of a real-time game is:
input → update state → draw state
Input should change an intent value. The update applies that intent. Drawing reads the resulting position:
player_x: f32 = 380
speed: f32 = 180
for !rl.WindowShouldClose() {
direction: f32 = 0
if rl.IsKeyDown(.LEFT) do direction = -1
if rl.IsKeyDown(.RIGHT) do direction = 1
dt := rl.GetFrameTime()
player_x += direction * speed * dt
rl.BeginDrawing()
rl.ClearBackground(rl.RAYWHITE)
rl.DrawCircle(i32(player_x), 225, 20, rl.RED)
rl.EndDrawing()
}
GetFrameTime returns elapsed seconds since the previous frame. The equation position += direction × speed × dt produces pixels per second rather than pixels per frame. A monitor rendering at 30 fps and one rendering at 120 fps therefore produce approximately the same real-world speed.
IsKeyDown is appropriate for continuous movement. IsKeyPressed reports the one frame in which a key changed from up to down; it is better for a menu choice, restart command, or a Snake turn request.
Coordinates and Draw Order
The screen origin is the upper-left. x increases rightward and y increases downward. raylib’s DrawCircle takes integer centre coordinates, radius, and colour; i32(player_x) makes the conversion from simulation’s floating-point position explicit.
Later drawing calls appear above earlier ones. A minimal frame normally clears the background, draws world objects, and then draws screen-space interface text. raylib does not retain visual objects between frames. The data in the program—not last frame’s pixels—is the source of truth.
Audio Has the Same Lifecycle
Sound resources require an audio device and must be unloaded before it closes:
rl.InitAudioDevice()
defer rl.CloseAudioDevice()
eat_sound := rl.LoadSound("assets/eat.wav")
defer rl.UnloadSound(eat_sound)
Load once during startup, then call rl.PlaySound(eat_sound) when the relevant game event occurs. Music streams require rl.UpdateMusicStream(music) every frame while they play. The next note applies the window loop, input distinction, elapsed time, grid coordinates, and dynamic arrays to a complete Snake game.