Learning

Odin & raylib · Reference

raylib Quick Reference

The raylib procedures a 2D game actually uses, with their real Odin signatures.

Signatures below are taken from the bindings that ship with the compiler (/usr/lib/odin/vendor/raylib/raylib.odin, raylib 6.0). rl is the conventional alias for import rl "vendor:raylib".

The fastest lookup you have

Nothing on this page beats grepping the bindings directly. Every procedure carries its original raylib comment.

grep -n "Draw.*Circle" /usr/lib/odin/vendor/raylib/raylib.odin
grep -n "Collision"    /usr/lib/odin/vendor/raylib/raylib.odin

The skeleton

package main

import rl "vendor:raylib"

main :: proc() {
	rl.SetConfigFlags({.VSYNC_HINT})       // before InitWindow
	rl.InitWindow(960, 540, "Title")
	defer rl.CloseWindow()

	rl.InitAudioDevice()                   // only if you need sound
	defer rl.CloseAudioDevice()

	for !rl.WindowShouldClose() {
		dt := rl.GetFrameTime()
		// update

		rl.BeginDrawing()
		defer rl.EndDrawing()

		rl.ClearBackground(rl.RAYWHITE)
		// draw

		free_all(context.temp_allocator)
	}
}

Core types

Vector2   :: [2]f32                       // an array, so + - * / work component-wise
Color     :: distinct [4]u8               // r, g, b, a

Rectangle :: struct { x, y, width, height: f32 }   // x,y is the TOP-LEFT corner

Camera2D :: struct {
	offset:   Vector2,    // where `target` lands on screen
	target:   Vector2,    // world point the camera looks at
	rotation: f32,        // degrees
	zoom:     f32,        // 1.0 is unscaled — a zoom of 0 draws nothing
}

Built-in colours: RAYWHITE WHITE BLACK BLANK LIGHTGRAY GRAY DARKGRAY YELLOW GOLD ORANGE PINK RED MAROON GREEN LIME DARKGREEN SKYBLUE BLUE DARKBLUE PURPLE VIOLET DARKPURPLE BEIGE BROWN DARKBROWN MAGENTA. Build your own with rl.Color{40, 42, 54, 255}, or fade one with rl.Fade(rl.RED, 0.5).

Window and timing

Procedure Signature
InitWindow proc(width, height: c.int, title: cstring)
CloseWindow proc()
WindowShouldClose proc() -> bool
SetConfigFlags proc(flags: ConfigFlags) — a bit_set, call before InitWindow
SetTargetFPS proc(fps: c.int)
GetFrameTime proc() -> f32 — seconds since last frame
GetTime proc() -> f64 — seconds since InitWindow
GetScreenWidth / GetScreenHeight proc() -> c.int
IsWindowResized proc() -> bool

Useful ConfigFlag members: .VSYNC_HINT .WINDOW_RESIZABLE .MSAA_4X_HINT .FULLSCREEN_MODE .BORDERLESS_WINDOWED_MODE .WINDOW_HIGHDPI.

Input

Procedure Signature
IsKeyDown proc(key: KeyboardKey) -> bool — held
IsKeyPressed proc(key: KeyboardKey) -> bool — the frame it went down
IsKeyReleased proc(key: KeyboardKey) -> bool
IsMouseButtonDown / IsMouseButtonPressed proc(button: MouseButton) -> bool
GetMousePosition proc() -> Vector2
GetMouseWheelMove proc() -> f32

The enum type is inferred, so write rl.IsKeyDown(.SPACE). Key names are the raylib ones with KEY_ stripped: .LEFT .RIGHT .UP .DOWN .SPACE .ENTER .ESCAPE .A .Z .ZERO .NINE .LEFT_SHIFT .F1. Mouse buttons are .LEFT .RIGHT .MIDDLE.

IsKeyDown vs IsKeyPressed is the distinction that causes the most bugs. Movement wants Down. Jumping, shooting, opening a menu want Pressed.

Drawing shapes

Procedure Signature
ClearBackground proc(color: Color)
DrawRectangleV proc(position, size: Vector2, color: Color)
DrawRectangleRec proc(rec: Rectangle, color: Color)
DrawRectangleLinesEx proc(rec: Rectangle, lineThick: f32, color: Color)
DrawCircleV proc(center: Vector2, radius: f32, color: Color)
DrawCircleLinesV proc(center: Vector2, radius: f32, color: Color)
DrawLineV proc(startPos, endPos: Vector2, color: Color)

Everything draws from the top-left and the y-axis points down. To centre a PLAYER_SIZE box on pos, draw at pos - PLAYER_SIZE/2.

Text

Procedure Signature
DrawText proc(text: cstring, posX, posY, fontSize: c.int, color: Color)
MeasureText proc(text: cstring, fontSize: c.int) -> c.int
DrawFPS proc(posX, posY: c.int)
LoadFont proc(fileName: cstring) -> Font
DrawTextEx proc(font: Font, text: cstring, position: Vector2, fontSize, spacing: f32, tint: Color)

Drawing a number requires a cstring, and core:fmt has one built for exactly this:

import "core:fmt"

label := fmt.ctprintf("Score: %v", score)
rl.DrawText(label, 8, 8, 20, rl.BLACK)

ctprintf allocates from the temp allocator, so call free_all(context.temp_allocator) once at the end of every frame.

Textures

Procedure Signature
LoadTexture proc(fileName: cstring) -> Texture2D — after InitWindow
UnloadTexture proc(texture: Texture2D)
DrawTextureV proc(texture: Texture2D, position: Vector2, tint: Color)
DrawTextureEx proc(texture: Texture2D, position: Vector2, rotation, scale: f32, tint: Color)
DrawTexturePro proc(texture: Texture2D, source, dest: Rectangle, origin: Vector2, rotation: f32, tint: Color)

DrawTexturePro is the one that does everything: source picks a sub-rectangle of the image (this is how sprite sheets work), dest places and scales it, origin sets the pivot for rotation. Use rl.WHITE as tint to draw unmodified.

Textures need a GPU context, so load them after InitWindow, not before.

Collision

Procedure Signature
CheckCollisionRecs proc(rec1, rec2: Rectangle) -> bool
CheckCollisionCircles proc(center1: Vector2, radius1: f32, center2: Vector2, radius2: f32) -> bool
CheckCollisionCircleRec proc(center: Vector2, radius: f32, rec: Rectangle) -> bool
CheckCollisionPointRec proc(point: Vector2, rec: Rectangle) -> bool
GetCollisionRec proc(rec1, rec2: Rectangle) -> Rectangle — the overlap

GetCollisionRec is how you resolve a collision rather than just detect it: the returned rectangle tells you how deep the overlap is on each axis, and you push out along the shallower one.

Camera

camera := rl.Camera2D{ zoom = 1 }        // zoom = 0 draws nothing. Set it.

camera.target = player                    // follow the player
camera.offset = {WINDOW_WIDTH/2, WINDOW_HEIGHT/2}

rl.BeginMode2D(camera)
	// world-space drawing
rl.EndMode2D()
// screen-space drawing (HUD) goes here, outside the camera

GetScreenToWorld2D(position, camera) converts a mouse position into world space — essential the moment the camera moves. GetWorldToScreen2D goes the other way.

Audio

Procedure Signature
InitAudioDevice / CloseAudioDevice proc()
LoadSound proc(fileName: cstring) -> Sound
PlaySound proc(sound: Sound)
UnloadSound proc(sound: Sound)
LoadMusicStream proc(fileName: cstring) -> Music — streamed, needs UpdateMusicStream each frame

Short effects are Sound (decoded into memory). Long tracks are Music (streamed).

Vector maths

The bindings ship a raymath.odin, but prefer Odin’s own core:math/linalg. Because Vector2 is just [2]f32, linalg works on it directly, and around a third of raymath’s procedures are now marked deprecated in favour of a linalg equivalent — rl.Vector2Lerp compiles with a warning telling you to use linalg.lerp.

import "core:math"
import "core:math/linalg"

dir  := linalg.normalize0(target - player)   // 0 version is safe on a zero vector
dist := linalg.length(target - player)
mid  := linalg.lerp(a, b, 0.5)
ang  := math.atan2(dir.y, dir.x)

x := clamp(player.x, 0, WINDOW_WIDTH)        // builtin, no import

Where to look things up

Related: Odin cheatsheet · Lesson 1 · Lesson 2