packages/app/odin-raylib-game-template
o

odin-raylib-game-template

bf93be9app

Hot-reload game template for Odin + Raylib, with an optional generic ECS. A batteries-included starting point for making games in Odin.

0 stars0 forksZlibupdated 7 hours ago
Open repo

Odin + Raylib Game Template

Odin nightly raylib vendored platform Windows tests: 16 passing license: zlib

A starting point for making games in Odin with raylib, modeled on the classic raylib-game-template — plus hot code reloading (edit gameplay code and watch it change in a running window), an optional ECS, and a guide to growing your code from placeholder to real game, with a bridge for developers coming from Python or JavaScript.

Screen flow: logo advances to title, which branches to options, gameplay (then ending), and the optional ECS demo — all returning to title

Requirements

  • Odin on your PATH (raylib ships with Odin as vendor:raylib — nothing else to install). Check with:
    odin version
    
  • Windows (the build scripts are .bat). The game code is cross-platform; to port, translate the scripts to .sh and change the hot-reload host's DLL name (GAME_DLL :: "game.dll" in source/main_hot_reload/main_hot_reload.odin) to the platform's shared-library extension (.so on Linux, .dylib on macOS).

Quick start

build_hot_reload.bat

This builds the game as game.dll, builds and launches the development host, and opens the window. Then:

  1. Leave the game running.
  2. Edit any source/screen_*.odin file (try changing text on the gameplay screen).
  3. In a second terminal, run build_hot_reload.bat again — it rebuilds only the DLL and exits.
  4. Watch the running window pick up your change without restarting.

In the window: F5 forces a reload, F6 restarts with fresh state, ESC quits.

Build scripts

Script What it produces
build_hot_reload.bat game.dll + game_hot_reload.exe (dev host). Your everyday command.
build_debug.bat build/game_debug.exe — one file, unoptimized, symbols + console.
build_release.bat build/game_release.exe — one file, optimized, no console window.

Project tour

source/
  game.odin            The exported hot-reload API + the frame loop + Game_Memory.
  screen_manager.odin  The Screen enum, fade transitions, and dispatch.
  screen_logo.odin     ┐
  screen_title.odin    │ One file per screen. Each has init_/update_/draw_ procs
  screen_options.odin  │ and a small *_State struct stored on Game_Memory.
  screen_gameplay.odin │ ← build your game here.
  screen_ending.odin   ┘
  main_hot_reload/     The dev host: loads game.dll, watches it, reloads live.
  main_release/        The release host: statically links the game into one exe.
assets/                Textures, fonts, sounds (none required to run).

The golden rule

All mutable game state lives on Game_Memory, reached through the global g: ^Game_Memory. Never make a mutable package-level variable.

Why: hot reload swaps the code, but your state has to survive the swap. The host keeps the Game_Memory block alive and hands the pointer back to the new code. A plain package global would be reset to zero on every reload; a field on Game_Memory persists. (Constants declared with :: are fine — they're not state.)

// NO — resets to 0 on every hot reload:
player_score: int

// YES — lives on Game_Memory, survives reloads:
Gameplay_State :: struct {
	player_score: int,
}
// …reached as g.gameplay.player_score

How to organize a growing game

You don't need any of this on day one. The template starts with placeholder screens on purpose. Reach for the next step only when your current structure starts to hurt.

1. Start with structs on Game_Memory

A "character" is just data. Put it in the gameplay state:

Player :: struct {
	pos:    rl.Vector2,
	health: int,
}

Gameplay_State :: struct {
	player: Player,
}

Update and draw it from update_gameplay / draw_gameplay.

2. Many things → a dynamic array of entities

When you have lots of similar objects (enemies, bullets, pickups), use a [dynamic]T (a growable list — see the Python/JS bridge below):

Entity :: struct {
	pos:    rl.Vector2,
	vel:    rl.Vector2,
	kind:   Entity_Kind,
	health: int,
	alive:  bool,
}

Gameplay_State :: struct {
	player:   Player,
	entities: [dynamic]Entity,
}

Create it in init_gameplay (g.gameplay.entities = make([dynamic]Entity)), and free it when you leave the screen.

When the entity array starts to strain — many object kinds, components that come and go — that's the moment to reach for the optional ECS (see "Optional: Entity Component System" below).

3. A world / level module

When you have tiles, rooms, or maps, give them their own file — say world.odin — that owns the level data:

// world.odin
package game

Level :: struct {
	width, height: int,
	tiles:         [dynamic]Tile,
}

load_level :: proc(index: int) -> Level { /**/ }
draw_level :: proc(level: ^Level)       { /**/ }

Store the current Level on Gameplay_State. Screens ask the world what to draw instead of knowing tile details themselves.

4. Components for cross-cutting concerns

health, inventory, and score show up on many kinds of things. Model them as small structs you compose into entities, rather than copy-pasting fields:

Health    :: struct { current, max: int }
Inventory :: struct { items: [dynamic]Item }

Player :: struct {
	pos:       rl.Vector2,
	health:    Health,
	inventory: Inventory,
}

Introduce a component the moment two different kinds of entity need it.

5. A behaviors / AI layer

Put decision-making in its own file — behaviors.odin — as functions that take an entity plus the world and decide what it does. This keeps AI decoupled from rendering, and is the natural home for the algorithms this workspace is about (state machines, steering, pathfinding):

// behaviors.odin
package game

update_chaser :: proc(e: ^Entity, target: rl.Vector2) {
	dir := rl.Vector2Normalize(target - e.pos)
	e.vel = dir * CHASER_SPEED
}

6. Systems over the entity list

Once behaviors multiply, run them as passes over all entities each frame, and keep the per-screen update_* thin:

update_gameplay :: proc() {
	update_input(&g.gameplay.player)
	update_behaviors(g.gameplay.entities[:], g.gameplay.player.pos)
	update_movement(g.gameplay.entities[:])
	update_combat(&g.gameplay)
}

7. Where assets and config live

Centralize tuning constants (CHASER_SPEED ::), asset handles, and lookup tables so screens and systems refer to them by name rather than hard-coding values in the middle of logic. Load asset handles in game_init, store them on Game_Memory, and unload them in game_shutdown.

The through-line: each file has one job, everything mutable flows through g, and you add a module only when a concept has earned one.


Optional: Entity Component System (ECS)

The template ships an optional generic ECS in source/ecs/ (package ecs). It's entirely opt-in: the default logo→title→gameplay→ending flow doesn't use it. Press E on the title screen to see the live demo (source/screen_ecs_demo.odin) — hold left-click to spawn particles; watch the entity count rise and fall as they expire.

The ECS particle demo: colorful glowing entities on a dark field with a live entity count

Illustrative — each dot is an entity with Position, Velocity, Tint, and Lifetime components. Press E from the title to run it for real.

An ECS stores your game objects as entities (just ids) with components (plain data structs) attached, and runs systems (functions) over everything that has a given set of components. It shines once you have many objects of varying makeup. Here's the whole API:

import "ecs"

// 1. Define components — any plain struct:
Position :: struct { x, y: f32 }
Velocity :: struct { x, y: f32 }
Health   :: struct { hp: int }

// 2. Register them, in a fixed order (see the reload note below):
register_components :: proc(w: ^ecs.World) {
	ecs.begin_register(w)
	ecs.register(w, Position)
	ecs.register(w, Velocity)
	ecs.register(w, Health)
}

// 3. Make a world, initialize it once, and register your components:
world: ecs.World
ecs.world_init(&world)          // allocates internal storage — call once
defer ecs.world_destroy(&world) // free when done (in a game: in game_shutdown)
w := &world                     // the ecs procs take a ^World
register_components(w)

// 4. Create entities and attach components:
e := ecs.create(w)
ecs.add(w, e, Position{100, 100})
ecs.add(w, e, Velocity{40, 0})

ecs.get(w, e, Position)   // -> ^Position (nil if absent)
ecs.has(w, e, Velocity)   // -> bool
ecs.remove(w, e, Health)  // detach one component
ecs.destroy(w, e)         // remove the entity; stale handles to `e` go invalid

// 5. Systems query for a component set and operate:
move :: proc(w: ^ecs.World, dt: f32) {
	for e in ecs.query(w, Position, Velocity) {
		p := ecs.get(w, e, Position)
		v := ecs.get(w, e, Velocity)
		p.x += v.x * dt
		p.y += v.y * dt
	}
}

Adding a component to your game is just: define a struct, add one ecs.register line, write a system that queries it. There's no central World struct to edit.

Entities are safe handles

ecs.create returns an Entity (an index + a generation). When you destroy an entity its slot is reused later, but the generation is bumped — so any old handle to the destroyed entity fails ecs.is_alive and ecs.get returns nil. No dangling references.

Storing the world (and the hot-reload note)

Keep your ecs.World on Game_Memory (the demo uses g.ecs_demo.world) so it survives hot reloads, and free it in game_shutdown with ecs.world_destroy.

One detail makes generic ECS coexist with hot reload: Odin's runtime type ids can change when the DLL recompiles, so the ECS rebuilds its type→pool mapping each load by re-running your register calls (that's why registration is an explicit, ordered step). Your entity data itself is preserved. The demo simply calls ecs_demo_register at the top of its update every frame — cheap and always reload-safe. In your own game you can do the same, or call it once at startup and again from a reload hook. Note: changing a component's fields (its size/layout) can't be applied by a hot reload — press F6 to restart when you edit a component struct.

Running the ECS tests

The ecs package has unit tests:

odin test source/ecs

Coming from Python or JavaScript? Your new mental model

Odin is a compiled, statically-typed systems language. If your background is Python or JS, the biggest adjustments are about the workflow — how code runs, how projects are packaged, how you get libraries — as much as the syntax. Here's the shift.

Running code: you compile, you don't interpret

There's no python app.py / node app.js interpreter and no REPL. Source is compiled to a native .exe ahead of time.

  • odin run source/main_release — compile and run once (closest thing to python app.py).
  • odin build source/main_release — just produce the executable.

(You point Odin at a directory that has a main proc — here the release host. The source/ directory itself is package game with no main, so it's built as a library/DLL by the build scripts, not run directly.)

In this template you rarely call those directly — the .bat scripts do — and the hot-reload loop is your interactive iteration: edit, rebuild the DLL, watch the running game update. That's the moral equivalent of re-running a script, but without losing your place.

Packages & imports: directories, not files or registries

A package is a directory of .odin files that share a namespace. Every file in source/ starts with package game — so they see each other's types and procs with no imports between them. You only import to reach another directory:

import rl "vendor:raylib"   // a bundled collection
import "core:fmt"           // the standard library — always available

core: (standard library) and vendor: (bundled third-party like raylib) come with your Odin install, the way Python's stdlib is always there.

Dependencies & packaging: no pip, npm, venv, or lockfile

There is no central package manager, no requirements.txt / package.json, no node_modules, no virtualenv. You add a third-party library by dropping its source into your project and importing the directory (this is called vendoring). Fewer moving parts, no dependency-resolution step; you control versions by choosing which source you copy in.

The build system is the compiler

No webpack/vite, no setup.py/pyproject.toml, no transpile or bundle step. A one-line odin build is the build. The .bat files here are thin wrappers around it.

Types & structs — but no classes

Types are static, but usually inferred, so it still reads lightly:

x := 5            // x is an int, inferred — like Python
name := "Alice"   // a string

There are no classes, methods, or inheritance — just structs (data) and procs (functions) you pass data to:

# Python
class Player:
    def __init__(self, hp): self.hp = hp
    def hurt(self, n): self.hp -= n
// Odin
Player :: struct { hp: int }

hurt :: proc(p: ^Player, n: int) {   // pass the data in explicitly
	p.hp -= n
}

Memory & defer: no garbage collector

You allocate and free. defer schedules cleanup to run when the current scope exits — like a lightweight with/finally:

data := os.read_entire_file("save.dat", context.allocator) or_else nil
defer delete(data)   // runs automatically at end of scope

This template hides most memory management for you — state lives in one Game_Memory block allocated once — but it's worth knowing where the GC went.

Values vs. pointers

Assigning a struct copies it. In Python/JS "everything is a reference"; in Odin it isn't. Use ^T for a pointer type and &x to take an address when you want to share and mutate:

a := player      // a COPY of player
b := &player     // a pointer to the same player; b.hp = 0 changes the original

This is exactly why the template threads state through g: ^Game_Memory — one shared, mutable block.

Enums, unions, and errors

Prefer an enum (or a tagged union) for things you might model with magic strings or dicts elsewhere — like this template's Screen enum. Errors are returned as extra values, not thrown:

data, err := os.read_entire_file("x", context.allocator)
if err != nil {
	// handle it — no try/except
}

Quick reference

You know (Python / JS) In Odin
python app.py / node app.js odin run source/main_release (compile + run)
REPL / live reload the hot-reload loop (edit → rebuild DLL → live)
pip install / npm install vendor the library's source, import the dir
requirements.txt / package.json (none — no package manager)
module = file package = directory of .odin files
class + methods struct (data) + proc (functions)
everything is a reference values copy; use ^T / &x to share
None / null the zero value, or a Maybe(T) / tagged union
try / except / throw multiple return values: val, err := f()
print(...) fmt.println(...) (import "core:fmt")
list / [] [dynamic]T (growable) or [N]T (fixed)
dict / {} / Map map[K]V

New to raylib too? The raylib cheatsheet lists every function; they're available in Odin as rl.FunctionName.

License

zlib — the same permissive license as raylib. Use it for anything, including commercial projects; attribution is appreciated but not required.

Package Info
Version
bf93be9
License
Zlib
Author
@vasanthsarathy
Type
app
Forks
0
Created
10 hours ago
Updated
7 hours ago
Health
maintained
has releases
has readme
has license
Activity
last 56 weeks