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