Learning

Odin & raylib · Reference

Odin Cheatsheet

The Odin syntax you need for game code, written for someone arriving from a higher-level language.

Every snippet on this page compiles against Odin dev-2026-08. This is a lookup page, not a tutorial — skim it, then come back when you have forgotten something.

Declarations

Two operators. The difference is when the value exists.

MAX_HEALTH :: 100            // constant: compile time
Vec        :: [2]f32         // types are constants too
main       :: proc() { }     // so are procedures

count  := 0                  // variable, type inferred (int)
ratio: f32 = 0.5             // variable, type stated
zeroed: [4]u8                // variable, zero-initialised: [0, 0, 0, 0]
garbage: [4]u8 = ---         // explicitly uninitialised — you asked for it

Everything is zero-initialised by default. A zeroed bool is false, a zeroed pointer is nil, a zeroed slice is empty and safe to iterate, a zeroed map is empty and safe to read from.

Types

Category Types
Signed i8 i16 i32 i64 i128 int (pointer-sized)
Unsigned u8 u16 u32 u64 u128 uint uintptr
Float f16 f32 f64
Text string (Odin, length-carrying), cstring (C, NUL-terminated), rune (a codepoint)
Other bool b8 b32, rawptr, any
Explicit-endian u32le, i16be, …

int is 64-bit on a 64-bit machine. Numeric conversion is always explicit: f32(count), int(ratio). There is no implicit widening.

Composite types

Entity :: struct {
	pos:    [2]f32,
	health: int,
	name:   string,
}

Direction  :: enum { North, East, South, West }
Directions :: bit_set[Direction]        // a set of enum values, stored as bits
Shape      :: union { f32, [2]f32 }     // a tagged union
Form Syntax Notes
Fixed array [3]int Size is part of the type. Lives on the stack.
Slice []int Pointer + length. A view into something else.
Dynamic array [dynamic]int Growable, heap-allocated, must be deleted.
Map map[string]int Heap-allocated, must be deleted.
Pointer ^Entity &x takes an address, p^ dereferences.
Multi-pointer [^]u8 A C-style “pointer to many”, for C interop.
fixed := [3]int{1, 2, 3}
s     := fixed[:]              // slice viewing the whole array
part  := fixed[1:3]            // slice of elements 1 and 2

list: [dynamic]int
defer delete(list)
append(&list, 4, 5, 6)

scores: map[string]int
defer delete(scores)
scores["hero"] = 10
if value, ok := scores["hero"]; ok { /* found */ }
delete_key(&scores, "hero")

missing := scores["nobody"] or_else -1   // default instead of a zero value

Procedures

add :: proc(a, b: int) -> int {
	return a + b
}

divmod :: proc(a, b: int) -> (quotient, remainder: int) {
	return a / b, a % b
}

q, r := divmod(7, 2)                     // 3, 1

Arguments are passed by value. To mutate, pass a pointer: proc(e: ^Entity). Default and named arguments both work: spawn(pos = {0, 0}, health = 10).

Control flow

if health <= 0 { die() }
if value, ok := lookup(key); ok { use(value) }    // if with an init statement

for !done { }                        // while
for i := 0; i < 10; i += 1 { }       // three-part
for i in 0 ..< 10 { }                // half-open range: 0 to 9
for i in 0 ..= 10 { }                // closed range: 0 to 10
for item in items { }                // by value
for item, index in items { }         // with index
for &item in items { }               // by pointer, so you can mutate
for { break }                        // infinite

switch dir {
case .North, .South: vertical()
case .East, .West:   horizontal()
case:                fallback()      // default
}

Odin’s switch does not fall through. Write fallthrough if you want it. A switch with no operand is a clean else if chain. Type-switch a union with switch v in shape.

defer

Runs at the end of the enclosing scope, in reverse order of registration.

f, err := os.open("save.dat")
defer os.close(f)                    // runs when the procedure returns

for entity in entities {
	rl.BeginMode2D(camera)
	defer rl.EndMode2D()             // runs at the end of THIS iteration
}

A diverging call (os.exit, panic) at the end of a scope makes earlier defers unreachable, and the compiler will tell you so.

Memory

Odin has no garbage collector. Four calls cover almost everything:

Call Frees with Use for
new(T) free(p) One heap value, returns ^T
make([]T, n) delete(s) A slice of n zeroed elements
make(map[K]V) delete(m) A map (declaring one is enough; make presizes)
append(&arr, v) delete(arr) Growing a [dynamic]T

Every allocating call takes an implicit allocator from the context. The one worth knowing early is the temp allocator — a scratch arena you throw away wholesale:

label := fmt.ctprintf("Score: %v", score)   // allocated in the temp allocator
rl.DrawText(label, 8, 8, 20, rl.BLACK)
// ...at the very end of each frame:
free_all(context.temp_allocator)

fmt.ctprintf is the answer to “how do I draw a number?” — it formats and returns a cstring that raylib accepts directly. Because it uses temp memory, call free_all(context.temp_allocator) once per frame or it grows forever.

Errors

Odin has no exceptions. Procedures return an error value alongside the result:

data, err := os.read_entire_file("level.json", context.allocator)
if err != nil { return }
defer delete(data)

value := parse(text) or_else 0       // substitute a default

or_return propagates an error to the caller instead of handling it. It requires the enclosing procedure’s return values to be named:

load :: proc() -> (level: Level, err: Error) {
	data := read_file("level.json") or_return   // returns early on error
	return decode(data), nil
}

Strings

name: string  = "hero"               // Odin string: pointer + length, not NUL-terminated
title: cstring = "hero"              // C string: literals convert to either

s  := string(c_str)                  // cstring -> string (cheap, computes length)
cs := strings.clone_to_cstring(s)    // string -> cstring (allocates; delete it)

len(s) on a string is bytes, not characters. Iterate with for r in s to get runes.

Command line

Command What it does
odin run . Compile the current directory’s package and run it
odin build . Compile only
odin check . Type-check without producing a binary — fastest feedback
odin test . Run procedures marked @(test)
odin doc . Print package documentation

Flags worth knowing:

Flag Effect
-vet Extra static checks. Turn it on and leave it on.
-debug Emit debug info, for gdb/lldb
-o:speed Optimise. Default is -o:minimal, which is fast to build and slow to run.
-out:name Name the output binary
-strict-style Enforce the official formatting rules

odinfmt (sudo pacman -S odinfmt) formats source to the official style: tabs for indentation, no trailing whitespace.

Where to look things up

Related: raylib quick reference · Lesson 1 · Lesson 2