T
ThePrimeagen
#Odin programming language#game development#raylib

Odin Programming Language for Game Development: A Deep Dive

Explore the Odin programming language for game development, comparing it to Lua and C. Learn about its data-oriented design, vendor imports, and explicit function overloading for building robust games.

5 min readAI Guide

Introduction

The Odin programming language offers a C-like alternative specifically designed for game development, emphasizing data-oriented design and high performance. It simplifies integrating external libraries through its vendor import system and enhances code clarity with explicit function overloading, making it a compelling choice for building robust and efficient games.

Configuration Checklist

Element Version / Link
Language / Runtime Odin Programming Language
Main library raylib
Required APIs JSON (core library), raylib (controls, frame rate, drawing)
Keys / credentials needed None mentioned

Step-by-Step Guide

Step 1 — Setting up the Odin Environment

To begin game development with Odin, ensure the Odin compiler is installed. The video implies a standard installation, and then demonstrates running an Odin program from the command line.

odin run .
  • odin run .: Compiles and executes the Odin program in the current directory.

Step 2 — Importing External Libraries with vendor

Step 2 — Importing External Libraries with vendor
Odin simplifies the inclusion of external C libraries by providing a vendor directory. This allows developers to easily access a wide range of pre-built functionalities without complex build configurations.

package game

import "assets"
import "constants"
import "random"
import rl "vendor:raylib" // Import raylib from the vendor directory, aliasing it as 'rl'
import "vendor:libc"      // Example: import the standard C library
import "vendor:box2d"     // Example: import Box2D for 2D physics
// [Editor's note: Other vendor imports mentioned include sdl3, egl, lua, stb, x11, ENet, curl, ggpo, glfw, wasm, wgpu, zlib, cglft, OpenGL, darwin, nanovg, vulkan, microui, windows. Verify specific usage in official documentation.]
  • import rl "vendor:raylib": Imports the raylib library, which is pre-packaged within Odin's vendor system, and assigns it the alias rl for easier use.
  • import "vendor:libc": Imports the standard C library, demonstrating access to common C functionalities.
  • import "vendor:box2d": Imports the Box2D physics engine, highlighting Odin's ease of integrating game-specific libraries.

Step 3 — Implementing UI and Game Logic with raylib

Step 3 — Implementing UI and Game Logic with raylib
raylib provides a simple and easy-to-use API for game programming, handling aspects like controls, frame rate, and drawing. This allows developers to focus on game-specific logic rather than low-level graphics programming.

// Example of a cell drawing function, demonstrating C-like programming with raylib
cell_draw :: proc(cell: ^Cell) {
    assert(cell.sprite != nil, "cell_draw: cell.sprite = nil") // Ensure sprite exists

    overhang := f32(assets.OVERDRAW_SIZE - assets.TEXTURE_SIZE) // Calculate overhang for drawing
    x_offset := f32(cell.col * C.GRID_PX_SIZE) - overhang      // Calculate X offset based on column and grid size
    if cell.reverse {
        x_offset = f32(assets.OVERDRAW_SIZE * 2) - x_offset    // Adjust X offset if cell is reversed
    }

    height := f32(cell.row * C.GRID_PX_SIZE) - overhang - height // Calculate height based on row and grid size

    sprite := cell.sprite // Get the sprite from the cell

    rl.SetTextureFilter(sprite.texture, rl.TEXTURE_POINT) // Set texture filtering to point for pixel art

    dest_w := f32(assets.OVERDRAW_SIZE) // Destination width
    if cell.reverse {
        dest_w = -dest_w // Flip width if cell is reversed
    }
    dest_h := f32(assets.OVERDRAW_SIZE) // Destination height

    rl.DrawTexturePro(sprite.texture, sprite.rect, rl.Rectangle{x_offset, pos.y, dest_w, dest_h}, rl.Vector2{0, 0}, 0, rl.WHITE) // Draw the texture with advanced options
    // rl.Rectangle{x_offset, pos.y, dest_w, dest_h} is the destination rectangle
    // rl.Vector2{0, 0} is the origin for rotation (not used here)
    // 0 is the rotation angle
    // rl.WHITE is the tint color
}
  • assert(cell.sprite != nil, "cell_draw: cell.sprite = nil"): Ensures that the cell has a valid sprite before attempting to draw it, preventing runtime errors.
  • overhang := f32(assets.OVERDRAW_SIZE - assets.TEXTURE_SIZE): Calculates an overhang value, likely for visual adjustments in pixel art rendering.
  • x_offset := f32(cell.col * C.GRID_PX_SIZE) - overhang: Determines the horizontal position for drawing the cell based on its column index and grid size.
  • if cell.reverse { x_offset = f32(assets.OVERDRAW_SIZE * 2) - x_offset }: Conditionally adjusts the x_offset if the cell needs to be visually reversed.
  • rl.SetTextureFilter(sprite.texture, rl.TEXTURE_POINT): Sets the texture filtering mode to TEXTURE_POINT, which is crucial for maintaining crisp pixel art appearance.
  • dest_w := f32(assets.OVERDRAW_SIZE): Initializes the destination width for the sprite.
  • if cell.reverse { dest_w = -dest_w }: Flips the sprite horizontally by negating its width if cell.reverse is true.
  • rl.DrawTexturePro(...): A raylib function to draw a texture with advanced parameters, including source and destination rectangles, origin, rotation, and tint.

Step 4 — Leveraging Explicit Function Overloading

Odin supports explicit function overloading, allowing multiple functions with the same name but different parameter types. This improves code organization and readability by clearly defining which functions can be overloaded.

// Define a procedure (proc) named 'cell_index' that can be overloaded by other functions
cell_index :: proc {
    cell_coord_to_index,    // Overload for Coord type
    cell_row_col_to_index,  // Overload for row and col integers
}

// Function to convert a Coord struct to an integer index
cell_coord_to_index :: #force_inline proc(coord: Coord) -> int {
    return cell_row_col_to_index(coord.row, coord.col) // Delegates to the row/col version
}

// Function to convert row and column integers to an integer index
cell_row_col_to_index :: #force_inline proc(row: int, col: int) -> int {
    return row * C.GRID_COLS + col // Calculates a linear index from 2D coordinates
}

// Example usage in a loop
for c in data.cells { // 'c' is a Cell struct
    idx := cell_index(c) // Odin automatically calls cell_coord_to_index because Cell has 'using coords: Coord'
    // ... further processing
}
  • cell_index :: proc { ... }: Declares cell_index as an overloaded procedure, explicitly listing the functions that can serve as its implementations based on argument types.
  • cell_coord_to_index :: #force_inline proc(coord: Coord) -> int: Defines a function that takes a Coord struct and returns an integer index. #force_inline suggests the compiler should try to inline this function for performance.
  • cell_row_col_to_index :: #force_inline proc(row: int, col: int) -> int: Defines a function that takes separate row and col integers and calculates a linear index.
  • idx := cell_index(c): When cell_index is called with a Cell (c), Odin's using directive (see Step 5) allows the Cell to be implicitly converted to a Coord, leading to cell_coord_to_index being called.

Step 5 — Utilizing the using Keyword in Structs

The using keyword in Odin allows embedding a struct within another struct and promoting its fields directly to the outer struct's scope. This simplifies access to nested fields and enables implicit type conversions for functions.

// Define a simple coordinate struct
Coord :: struct {
    row: int,
    col: int,
}

// Define a Cell struct that uses the Coord struct
Cell :: struct {
    using coords: Coord, // Embeds Coord and promotes its fields (row, col)
    is_placeable: bool,
    is_path: bool,
    reverse: bool,
    sprite: ^assets.Sprite,
}

// Example of accessing fields and implicit conversion
my_cell: Cell
my_cell.row = 5 // Directly access 'row' from 'coords' due to 'using'
my_cell.col = 10 // Directly access 'col' from 'coords' due to 'using'

// If a function expects a 'Coord', you can pass 'my_cell' directly
// because 'using coords: Coord' makes Cell implicitly convertible to Coord.
// For example, if cell_index(coord: Coord) is defined, you can call:
// idx := cell_index(my_cell)
  • using coords: Coord: Embeds the Coord struct within Cell. This means Cell now has row and col fields directly accessible (e.g., my_cell.row) without needing my_cell.coords.row.
  • my_cell.row = 5: Demonstrates direct access to the row field of the embedded Coord struct.
  • idx := cell_index(my_cell): Shows how using enables implicit conversion. If cell_index expects a Coord, passing a Cell will automatically use its embedded Coord part.

Comparison Tables

Feature / Language Lua (with Love2D) Odin (with raylib)
Project Size Suitability Less suitable for large projects (70-80k lines) More suitable for large projects
Dependencies Requires Love2D framework Minimal dependencies, vendor system for easy C library integration
UI System Custom-built UI system (speaker's experience) raylib provides robust and convenient drawing interfaces
Animation System Custom-built (Teage's work) raylib offers animation capabilities
Game Programming Focus General-purpose scripting language Designed specifically for game programming
Ease of Use (2D) Can be complex for drawing/textures raylib makes 2D game development "pretty dang simple"
Texture Manipulation Less convenient for drawing to textures and manipulating source rectangles raylib allows easy drawing to textures and manipulating source/destination rectangles (flipping, scaling)
Function Overloading Not directly supported in Lua Explicitly supported and well-designed in Odin
Memory Management Automatic (garbage collection) Manual (C-like), but easy to pick up with basic understanding
AI (Copilot) Integration Not explicitly mentioned, but Lua is generally supported Works well, but AI still produces "crap code" sometimes

⚠️ Common Mistakes & Pitfalls

  1. Over-reliance on AI for code generation: While AI tools like Copilot can assist, they often produce suboptimal or "crap code."
    • Fix: Use AI as a co-pilot, not the primary developer. Understand the generated code and refactor it as needed to fit project standards and ensure correctness.
  2. Sticking with unsuitable languages for large projects: Using languages like Lua for very large codebases (e.g., 70-80k lines) can lead to maintainability issues and reduced joy in programming.
    • Fix: Evaluate language suitability for project scale early on. Consider languages designed for performance and large-scale development, like Odin, for complex game projects.
  3. Lack of understanding of manual memory management: Odin, being C-like, requires manual memory management. Beginners without this understanding might struggle.
    • Fix: Develop a basic mental model of how manual memory management works. Odin's design aims to make this more manageable, but foundational knowledge is key.

Glossary

Odin: A general-purpose programming language with distinct typing built for high performance, modern systems, and data-oriented programming, often described as a C alternative for the joy of programming.
raylib: A simple and easy-to-use library to enjoy videogames programming, providing functionalities for graphics, audio, input, and more.
Function Overloading: A feature in programming languages that allows multiple functions to have the same name but different parameters (number or type), with the correct function being called based on the arguments provided.

Key Takeaways

  • Odin is a compelling language for game development, offering a C-like experience with modern features.
  • The vendor import system in Odin simplifies integrating various C libraries (like raylib, box2d, libc) directly into your project.
  • raylib is highlighted as a superior library for 2D game development compared to Love2D, especially for texture manipulation and drawing.
  • Odin's explicit function overloading and using keyword in structs enhance code readability and maintainability by providing clear control over function behavior and field access.
  • Learning Odin is relatively easy for developers with a basic understanding of manual memory management.
  • AI tools like Copilot can be used with Odin, but human oversight and refactoring are crucial due to potential code quality issues.
  • The speaker emphasizes returning to "coding for the joy of coding" by focusing on personal projects and exploring new languages like Odin and potentially Jai.

Resources