Odin From Zero: Values, Procedures, and Data
The Odin fundamentals needed before opening a game window: declarations, types, procedures, enums, structs, dynamic arrays, and explicit cleanup.
Odin is a compiled systems language with simple, visible data flow. Values have types, procedures declare their inputs and outputs, and data that grows at runtime has an explicit owner. Those properties make it well suited to a small game: the program state is ordinary data rather than an engine-managed object graph.
Install and Run a Program
Install the Odin compiler and ensure odin is on the shell path. A directory containing a main.odin file is a package. This is the smallest runnable program:
package main
import "core:fmt"
main :: proc() {
fmt.println("Hello, Odin")
}
Run it from that directory with:
odin run .
package main identifies the executable package. import makes another package available. main :: proc() declares the entry procedure. :: introduces a named declaration; proc declares a procedure.
Values and Types
:= creates a local variable and infers its type. : declares the type explicitly. Both forms produce mutable local values unless :: is used for a constant declaration.
name := "Nagaraja" // inferred string
lives: i32 = 3 // explicit 32-bit signed integer
speed: f32 = 120.0 // 32-bit floating point number
CELL_SIZE :: 20 // compile-time constant
Use integers for discrete quantities such as array indexes, grid cells, scores, and counts. Use floating-point values for elapsed seconds, velocity, angles, and other continuously measured quantities. Keeping those domains distinct prevents a position measured in pixels from being mistaken for a grid cell.
Procedures and Pointers
A procedure declares parameter names, their types, and an optional return type:
clamp_score :: proc(score: i32) -> i32 {
if score < 0 do return 0
return score
}
Arguments are passed as values by default. A pointer, written ^T, lets a procedure change an existing T value:
add_score :: proc(score: ^i32, amount: i32) {
score^ += amount
}
score: i32 = 0
add_score(&score, 10)
&score takes the address of score; score^ accesses the value at that address. This makes mutation visible at the call site and procedure boundary.
Model a Game With Structs and Enums
A struct gives related state one owner. An enum restricts a value to a finite set:
Direction :: enum { Up, Down, Left, Right }
Player :: struct {
x, y: i32,
direction: Direction,
score: i32,
}
player := Player{x = 4, y = 7, direction = .Right}
player.x += 1
The .Right shorthand is valid because the expected type is Direction. This is safer than representing direction with arbitrary strings or numbers. A game update can switch over every allowed direction and the compiler knows the state cannot be anything else.
Fixed and Dynamic Arrays
Odin’s [2]i32 is an array with exactly two integers. It is useful for a grid coordinate:
Cell :: [2]i32
head := Cell{20, 15}
head.x += 1
[dynamic]T is a growable array. append adds to it, len reports its length, and delete releases its backing storage:
body: [dynamic]Cell
append(&body, Cell{20, 15})
append(&body, Cell{19, 15})
defer delete(body)
defer schedules its statement for the end of the current procedure. Put it immediately after acquiring resources, including dynamic-array storage, windows, sounds, and files. In a game, the dynamic array will become the ordered Snake body: element zero is the head and the final element is the tail.
Packages Keep the Program Legible
As a program grows, files in one directory can share a package and expose declarations to importing packages. Nāgarāja uses this layout:
main.odin startup and frame-loop coordination
game/ rules and persistent game state
render/ drawing code
audio/ loaded sound resources
An import alias records where an identifier comes from:
import g "nagaraja:game"
state := g.init_game_state()
The named collection is supplied when compiling: -collection:nagaraja=. maps nagaraja: to the project root. The next note uses the same language features with raylib to create an actual window.