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
- Odin Overview — the official language tour.
- pkg.odin-lang.org — generated docs for
coreandvendor. /usr/lib/odin/core/— the standard library source, on your disk, readable.
Related: raylib quick reference · Lesson 1 · Lesson 2