A simple asset manager
21 June 2025
There are many types of asset managers found in games. Today I will share what I have been using for my small projects.
The API
The API for this is dead simple. There are only a few function calls:
asset_get :: ($T: Type, name: string) -> *T;
asset_unload :: (asset: *$T) -> bool;
asset_unload :: (name: string) -> bool;
asset_free_all :: () -> int;
asset_get_procs :: (t: Type) -> Asset_Procs, bool;
Getting assets
asset_get() is the way to either load or fetch assets. The asset manager has a table of asset entries, with the key being a string.
Your key can be anything, but in my case it is just the file path to the asset. You can also use IDs or some other mechanism.
The expected way to call it is:
texture := asset_get(Texture, "assets/textures/my-texture.png");
This will first check if the hash map already contains the texture. If so, it will simply return a pointer. If not, it will allocate T on the heap and collect some overloads.
This asset manager does not require any inheritance or the like, but it does require you to specify two overloads: asset_load and asset_unload.
asset_load :: (texture: *Texture, name: string) -> bool {
/* ... */
}
asset_unload :: (texture: *Texture) {
/* ... */
}
In asset_get() it will store these procedures along with the asset, so it can be reloaded or unloaded at will without the need for any additional type info.
Unloading assets
In my implementation, this is quite simple. You pass either the name or asset pointer, and it will call asset_unload() on the entry.
If you want to free all assets, you can also call asset_free_all() and it will iterate over all entries and unload them.
Getting the procedures
Like I touched on earlier, whenever a new type of asset is loaded, it will collect the relevant procedures. Aside from the asset_load() and asset_unload() procedures, it also stores the initializer, which in Jai is the function which sets a struct's default values. With these three methods, we are ready to do hot reloading.
Hot Reloading
There are a lot of really complicated hot reload systems, but I think in most cases they are over-engineered. My current implementation is single-threaded, but it should also work if you have worker threads.
The jist is to set up a file watcher, and when you notice that an asset has changed, you do the following:
// Get the asset entry based on the path from the file watcher
it := get_asset_entry(change.full_path);
procs, proc_success := asset_get_procs(it.asset_type);
assert(proc_success);
procs.unload_proc(it.asset_ptr);
if procs.initializer_proc then procs.initializer_proc(it.asset_ptr);
success := procs.load_proc(xx it.asset_ptr, full_path);
We use the existing *T pointer to unload, reinitialize the struct back to its default values, and then reload the asset.
Reload Callbacks
Hot reloading works, but to be actually useful, we need a callback system.
The API for this is as follows:
asset_reloader_register_callback :: (asset: *void, callback: Asset_Reload_Callback, user_data: *void);
We give it the current asset pointer, and the callback we would like. After the reloader has reloaded the asset, we can fire off the callback like so:
if success {
for * cb: asset_reloader.callbacks {
if cb.asset == it.asset_ptr {
cb.callback(cb.asset, cb.user_data);
}
}
}
Here we just check if the asset that was updated was the one in the callback, and if so we update it to the new asset pointer.
I mostly use the callbacks to rebuild the graphics pipelines after the shader has been updated.
shader := asset_get(Shader, "assets/shaders/sprite.slang");
asset_reloader_register_callback(shader, sprite_renderer_reload_pipeline, null);
Improvements
No heap allocations
There are a few optimizations that you can make. One is to avoid allocating each asset on the heap. You could have an array of bytes, and when you load an asset type for the first time, you also store the runtime size, along with the procs.
You could even have an extra byte stored for each asset in the byte array to set whether the entry is taken or not. This is useful if you are unloading assets regularly and want to reuse spots in the array.
Reference counting
Personally, I am a fan of asset_get() mostly being a simple lookup (if the asset is already loaded), but depending on your needs, you may want reference counting.
In this case, you can add the reference counting to asset_load() and asset_unload().
Conclusion
This is a very simple system, and probably not suitable for developing triple A titles, but I think a system like this can go a long way.