PVG
23. Hot Reloading Integration — Program Video Games

23. Hot Reloading Integration

Hot Reloading Implementation

In the previous lesson, we covered the theory behind hot reloading. Now we'll implement it for our RPG module, walking through the actual code changes needed.

Project Structure Changes

First, reorganise the project structure:

rpg/
├── src/
│   ├── main.odin              # The cradle
│   ├── raylib_api/
│   │   └── raylib_api.odin    # Raylib function pointers
│   └── game/                   # DLL package
│       ├── game.odin          # Main game file
│       ├── entities.odin
│       ├── physics.odin
│       ├── player.odin
│       ├── debug.odin
│       └── ...
├── build/                      # DLL output directory
└── assets/

Move all game files except main.odin into the game/ directory.

The Game API Structure

Create the API structure in main.odin:

Game_API :: struct {
    lib: dynlib.Library,
    reload_count: int,
    last_write_time: os.File_Time,
    window_width: proc(game_state_memory: rawptr) -> i32,
    window_height: proc(game_state_memory: rawptr) -> i32,
    game_state_size: proc() -> int,
    game_pre_window_create: proc(game_state_memory: rawptr),
    game_post_window_create: proc(game_state_memory: rawptr),
    game_update: proc(game_state_memory: rawptr),
    game_render: proc(game_state_memory: rawptr),
    game_post_reload: proc(game_state_memory: rawptr, rl_api: ^raylib_api.Raylib_API),
}

DLL Constants

Add platform-specific constants:

DLL_PATH :: "build/"
DLL_NAME :: "game"

when ODIN_OS == .Windows {
    DLL_EXT :: ".dll"
} else when ODIN_OS == .Darwin {
    DLL_EXT :: ".dylib"
} else {
    DLL_EXT :: ".so"
}

DLL_PATH_FULL :: DLL_PATH + DLL_NAME + DLL_EXT

The Reload Function

Implement the reload logic:

game_api_reload :: proc(api: ^Game_API) -> (ok: bool) {
    // Copy DLL to prevent OS locking
    dll_copy_path := fmt.tprintf("%s%s_%d%s", DLL_PATH, DLL_NAME, api.reload_count, DLL_EXT)
    
    if copy_err := os2.copy_file(dll_copy_path, DLL_PATH_FULL); copy_err != nil {
        fmt.printfln("Failed to copy the DLL from `%s` to `%s`.", DLL_PATH_FULL, dll_copy_path)
        return false
    }
    
    // Load symbols from copied DLL
    if symbol_count, symbol_init_ok := dynlib.initialize_symbols(api, dll_copy_path); !symbol_init_ok {
        fmt.println(dynlib.last_error())
        return false
    }
    
    // Track DLL modification time
    if dll_time, dll_time_err := os.last_write_time_by_name(DLL_PATH_FULL); dll_time_err == nil {
        api.last_write_time = dll_time
    } else {
        fmt.printfln("Failed to read last write time of `%s`", DLL_PATH_FULL)
        return false
    }
    
    api.reload_count += 1
    fmt.println("DLL reloaded.")
    return true
}

The New Main Loop

Replace the entire main() function:

main :: proc() {
    rl_api := raylib_api.init()
    
    // Load API and allocate memory
    game_api: Game_API
    assert(game_api_reload(&game_api))
    
    game_state_slice := make([]u8, game_api.game_state_size())
    game_state_memory := raw_data(game_state_slice)
    
    // Initial reload hook
    game_api.game_post_reload(game_state_memory, &rl_api)
    
    // Window creation hooks
    game_api.game_pre_window_create(game_state_memory)
    
    width := game_api.window_width(game_state_memory)
    height := game_api.window_height(game_state_memory)
    
    rl.SetConfigFlags(rl.ConfigFlags{.VSYNC_HINT})
    rl.InitWindow(width, height, "PVG RPG")
    defer rl.CloseWindow()
    
    game_api.game_post_window_create(game_state_memory)
    
    // Game loop with hot reload check
    for !rl.WindowShouldClose() {
        // Check for DLL changes
        if dll_write_time, dll_write_time_err := os.last_write_time_by_name(DLL_PATH_FULL); dll_write_time_err == nil {
            if dll_write_time != game_api.last_write_time {
                if game_api_reload(&game_api) {
                    game_api.game_post_reload(game_state_memory, &rl_api)
                } else {
                    continue
                }
            }
        }
        
        game_api.game_update(game_state_memory)
        
        rl.BeginDrawing()
        rl.ClearBackground(rl.BLACK)
        game_api.game_render(game_state_memory)
        rl.EndDrawing()
    }
}

Game-Side Type Aliases

In game/game.odin, add type aliases and the Raylib API global:

package main

import "vendor:raylib"
import "../raylib_api"

Vec2 :: [2]f32
Vec3 :: [3]f32
Rect :: raylib.Rectangle
Mat3 :: matrix[3, 3]f32
Mat4 :: raylib.Matrix
Color :: raylib.Color
AABB :: raylib.BoundingBox
Texture :: raylib.Texture

rl: ^raylib_api.Raylib_API

Exported Hook Functions

Implement the exported functions:

@export
game_state_size :: proc() -> int {
    return size_of(Game_State)
}

@export
window_width :: proc(gs: ^Game_State) -> i32 {
    return i32(gs.window_width)
}

@export
window_height :: proc(gs: ^Game_State) -> i32 {
    return i32(gs.window_height)
}

@export
game_pre_window_create :: proc(gs: ^Game_State) {
    gs.window_width = 1280
    gs.window_height = 720
}

@export
game_post_reload :: proc(gs: ^Game_State, rl_api: ^raylib_api.Raylib_API) {
    rl = rl_api
}

Assets Initialisation

Move asset loading into game_post_window_create:

assets_init :: proc(es: ^Events_State, assets: ^Assets_State) {
    texture_load :: proc(es: ^Events_State, path: cstring, key: Texture_Key, textures: ^map[Texture_Key]raylib.Texture) {
        if key not_in textures {
            texture := rl.LoadTexture(path)
            
            if rl.IsTextureValid(texture) {
                textures[key] = texture
                events_enqueue(es, Event {
                    debug_message = fmt.tprintf("Loaded texture `%v`", key)
                })
            } else {
                panic(fmt.tprintfln("Failed to load texture: %s", path))
            }
        }
    }
    
    texture_load(es, "./assets/textures/test_image.png", .Test_Image, &assets.textures)
    texture_load(es, "./assets/textures/soldier_idle.png", .Soldier_Idle, &assets.textures)
    texture_load(es, "./assets/textures/soldier_walk.png", .Soldier_Walk, &assets.textures)
}

@export
game_post_window_create :: proc(gs: ^Game_State) {
    debug_init(&gs.debug)
    assets_init(&gs.events, &gs.assets)
    animations_init(&gs.animations, gs.assets.textures)
    
    gs.world.player_handle = player_init(&gs.world)
    
    // Camera setup
    gs.rendering.camera_base_offset = Vec3{0, 16, 20}
    gs.rendering.camera.up = Vec3{0, 1, 0}
    gs.rendering.camera.projection = .PERSPECTIVE
    gs.rendering.camera.fovy = 45
    
    // Test entity and scene loading...
}

World State Reorganisation

Replace individual entity and physics state with a unified World_State:

World_State :: struct {
    player_handle: Entity_Handle,
    
    entities: [dynamic]Entity,
    generations: [dynamic]int,
    unused_entity_handles: [dynamic]Entity_Handle,
    active_entities: [dynamic]int,
    
    static_geometry: [dynamic]OBB3,
    walkable_surfaces: [dynamic]Quad,
}

Game_State :: struct {
    window_width: f32,
    window_height: f32,
    
    time: Time_State,
    input: Input_State,
    events: Events_State,
    assets: Assets_State,
    animations: Animations_State,
    rendering: Rendering_State,
    
    world: World_State,
    
    debug: Debug_State,
}

Update Function Signatures

Update all functions to use World_State instead of separate parameters:

// entities.odin
entity_create :: proc(ws: ^World_State) -> Entity_Handle
entity_get :: proc(ws: ^World_State, handle: Entity_Handle) -> ^Entity
entity_destroy :: proc(ws: ^World_State, handle: Entity_Handle)
entities_update :: proc(ws: ^World_State, dt: f32)

// physics.odin
physics_calculate_next_position :: proc(ws: ^World_State, center, velocity: Vec3, radius: f32) -> Vec3
physics_update :: proc(ws: ^World_State)

// player.odin
player_init :: proc(ws: ^World_State) -> Entity_Handle

Debug State Handling

Update debug.odin to handle DLL reloading:

@(private="file")
ds: ^Debug_State

debug_init :: proc(state: ^Debug_State) {
    ds = state
    ds.font = rl.LoadFontEx("assets/fonts/Hasklig-Regular.ttf", 16, nil, 128)
}

debug_begin :: proc(state: ^Debug_State) {
    // DLL might have been reloaded, rebind the pointer
    ds = state
}

Call debug_begin() at the start of game_update:

@export
game_update :: proc(gs: ^Game_State) {
    debug_begin(&gs.debug)
    
    input_update(&gs.input)
    time_update(&gs.time)
    events_update(&gs.events)
    entities_update(&gs.world, gs.time.delta)
    
    player := entity_get(&gs.world, gs.world.player_handle)
    
    player_update(player, gs.input, gs.time.delta)
    physics_update(&gs.world)
    rendering_camera_update(&gs.rendering, gs.input, player.position, gs.time.delta)
}

The Raylib API File

Copy src/raylib_api/raylib_api.odin with the full Raylib API structure and initialisation.

Build Configuration

Update your build script to output the DLL to the build/ directory with the correct name and build the cradle as the main executable.

The build script for cradle remains the same.

For the DLL, use something like this:

odin build src/game -debug -build-mode:dll -out:build/game.dll