├── .gitignore ├── README.md ├── game.odin └── main.odin /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.dll 3 | *.lib 4 | *.exp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the code shown in my Odin Hot Reload article: https://zylinski.se/posts/hot-reload-gameplay-code/ 2 | 3 | **NOTE**: I have a more recent and much more in-depth template for hot reload here: https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template 4 | -------------------------------------------------------------------------------- /game.odin: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "core:fmt" 4 | 5 | // All the state of our game will live within this struct. In order for the hot 6 | // reload to work all the memory that the game uses must be transferrable from 7 | // one game DLL to the next when a hot reload occurs, which we can do when 8 | // all the game's memory live in here. 9 | GameMemory :: struct { 10 | some_state: int, 11 | } 12 | 13 | g_mem: ^GameMemory 14 | 15 | // This function dynamically allocates a block of memory that we’ll use to store 16 | // all the state in our game. We assign it to a global variable so we can use it 17 | // from the other functions. 18 | @(export) 19 | game_init :: proc() { 20 | g_mem = new(GameMemory) 21 | } 22 | 23 | // Here you do your simulation and rendering. Return false when you wish to 24 | // terminate the program. 25 | @(export) 26 | game_update :: proc() -> bool { 27 | // To try hot reload, have the main program running and recompile 28 | // the game DLL with -= instead of += below 29 | g_mem.some_state += 1 30 | fmt.println(g_mem.some_state) 31 | return true 32 | } 33 | 34 | // This is called by the main program when game_update has returned false and 35 | // the main loop has exited. Clean up your memory here. 36 | @(export) 37 | game_shutdown :: proc() { 38 | free(g_mem) 39 | } 40 | 41 | // Return the pointer to the game memory. When a hot reload occurs, then main 42 | // program needs to get hold of the game memory pointer, so that it can load 43 | // a new game DLL and tell that game DLL to use the same memory by feeding 44 | // game_hot_reloaded with the game memory pointer. 45 | @(export) 46 | game_memory :: proc() -> rawptr { 47 | return g_mem 48 | } 49 | 50 | // Run after a hot reload occurs. When that hot reload occurs a new game DLL 51 | // is loaded and that game DLL needs to use the same game memory as the 52 | // previous game DLL. Therefore this function is fed the GameMemory pointer. 53 | @(export) 54 | game_hot_reloaded :: proc(mem: ^GameMemory) { 55 | g_mem = mem 56 | } 57 | -------------------------------------------------------------------------------- /main.odin: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import "core:dynlib" 4 | import "core:os" 5 | import "core:fmt" 6 | import "core:c/libc" 7 | 8 | // The main program will load a game DLL and each frame check if it changed. 9 | // If it does change then it will load a new game DLL and use the code in that 10 | // DLL instead. It will give the new DLL the memory the old one used. 11 | main :: proc() { 12 | // We use this to number the loaded game DLL. It is incremented on each 13 | // game DLL reload. Whenever we load the game API the game DLL is copied 14 | // so we can load it without locking the original game.dll. 15 | game_api_version := 0 16 | game_api, game_api_ok := load_game_api(game_api_version) 17 | 18 | if !game_api_ok { 19 | fmt.println("Failed to load Game API") 20 | return 21 | } 22 | 23 | game_api_version += 1 24 | 25 | // Tell the game to start itself up! 26 | game_api.init() 27 | 28 | // same as while(true) in C 29 | for { 30 | // The update function of the game will update and render the game. 31 | // It should return false when we want to exit the program and break 32 | // the main loop. 33 | if game_api.update() == false { 34 | break 35 | } 36 | 37 | // Check the last write date of the game DLL. If the date is different 38 | // from the one on the current game API, then try to do a hot reload. 39 | last_game_write, last_game_write_err := os.last_write_time_by_name("game.dll") 40 | 41 | if last_game_write_err == os.ERROR_NONE && game_api.lib_write_time != last_game_write { 42 | // Load a new game API. Might sometimes fail due game.dll still 43 | // being written by the Odin compiler. In that case new_game_api_ok 44 | // will be false and we will trey again next frame. 45 | new_game_api, new_game_api_ok := load_game_api(game_api_version) 46 | 47 | if new_game_api_ok { 48 | // This fetches a pointer to the game memory in the OLD game DLL 49 | game_memory := game_api.memory() 50 | 51 | // Completely unload the game DLL. The game memory survives, 52 | // that memory will only be deallocated if we explicitly free it 53 | unload_game_api(game_api) 54 | 55 | // Replace the game_api with the new one, this will make update 56 | // use the new game API next frame. 57 | game_api = new_game_api 58 | 59 | // Tell the new game API to use the same game memory 60 | game_api.hot_reloaded(game_memory) 61 | 62 | // Bump the API version 63 | game_api_version += 1 64 | } 65 | } 66 | } 67 | 68 | // This will finally deallocate game memory and do other cleanup. 69 | game_api.shutdown() 70 | 71 | unload_game_api(game_api) 72 | } 73 | 74 | // This struct contains pointers to the different procedures that live in the 75 | // game DLL, see the game.odin code for docs on what they do. 76 | GameAPI :: struct { 77 | init: proc(), 78 | update: proc() -> bool, 79 | shutdown: proc(), 80 | memory: proc() -> rawptr, 81 | hot_reloaded: proc(rawptr), 82 | 83 | lib: dynlib.Library, 84 | 85 | // We use this in the main loop to know if the game DLL has been updated 86 | // and needs reloading. 87 | lib_write_time: os.File_Time, 88 | api_version: int, 89 | } 90 | 91 | load_game_api :: proc(api_version: int) -> (GameAPI, bool) { 92 | lib_last_write, lib_last_write_err := os.last_write_time_by_name("game.dll") 93 | 94 | if lib_last_write_err != os.ERROR_NONE { 95 | fmt.println("Could not fetch last write date of game.dll") 96 | return {}, false 97 | } 98 | 99 | // We cannot just load the game DLL directly. This would lock the game DLL 100 | // and you could no longer hot reload since the compiler can't write to 101 | // the game DLL. So we make a unique name based on api_version (a number 102 | // that is incremented for each DLL reload) and then copy the DLL to that 103 | // location. 104 | game_dll_name := fmt.tprintf("game_{0}.dll", api_version) 105 | 106 | // This quite often fails on the first attempt because our program tries 107 | // to copy it before the odin compiler has finished writing it. In that 108 | // case we will return and try again the next frame. 109 | // 110 | // Note: Here I use windows copy command, it's not the best solution, but 111 | // it is the most compact code for this sample. 112 | if libc.system(fmt.ctprintf("copy game.dll {0}", game_dll_name)) != 0 { 113 | fmt.println("Failed to copy game.dll to {0}", game_dll_name) 114 | return {}, false 115 | } 116 | 117 | // This loads the newly copied game DLL 118 | lib, lib_ok := dynlib.load_library(game_dll_name) 119 | 120 | if !lib_ok { 121 | fmt.println("Failed to load game library") 122 | return {}, false 123 | } 124 | 125 | // Fetches all those procedures we marked with @(export) inside the game DLL 126 | // Not that it needs to manually cast them to the correct signature. 127 | api := GameAPI { 128 | init = cast(proc())(dynlib.symbol_address(lib, "game_init") or_else nil), 129 | update = cast(proc() -> bool)(dynlib.symbol_address(lib, "game_update") or_else nil), 130 | shutdown = cast(proc())(dynlib.symbol_address(lib, "game_shutdown") or_else nil), 131 | memory = cast(proc() -> rawptr)(dynlib.symbol_address(lib, "game_memory") or_else nil), 132 | hot_reloaded = cast(proc(rawptr))(dynlib.symbol_address(lib, "game_hot_reloaded") or_else nil), 133 | 134 | lib = lib, 135 | lib_write_time = lib_last_write, 136 | api_version = api_version, 137 | } 138 | 139 | if api.init == nil || api.update == nil || api.shutdown == nil || api.memory == nil || api.hot_reloaded == nil { 140 | dynlib.unload_library(api.lib) 141 | fmt.println("Game DLL is missing required procedure") 142 | return {}, false 143 | } 144 | 145 | return api, true 146 | } 147 | 148 | unload_game_api :: proc(api: GameAPI) { 149 | if api.lib != nil { 150 | dynlib.unload_library(api.lib) 151 | } 152 | 153 | // Delete the copied game DLL. 154 | // 155 | // Note: Here I use windows copy command, it's not the best solution, but 156 | // it is the most compact code for this sample. 157 | if libc.system(fmt.ctprintf("del game_{0}.dll", api.api_version)) != 0 { 158 | fmt.println("Failed to remove game_{0}.dll copy", api.api_version) 159 | } 160 | } --------------------------------------------------------------------------------