Asset management
While prototyping rendering I had to load a bunch of different assets from disk. The problem I usually have with asset loading is that I end up having to pass a bunch of strings around, strings which can change at any point.
texture : = get_texture("metal" );
It's rather standard, but I personally don't like it. Instead I would like the assets to be known constants. So for this game I've decided to do something different.
Generating asset management code
I created a simple asset definition file: assets.ad
which contains definitions of all the assets my game needs:
// Fonts:
main_menu: Poppins.ttf, 24
dialog: ComicSans.ttf, 18
...
// Textures:
metal: Metal128.png
wood: Light_wood.png
brick: Brick_wall.png
glass: Glass.png
// Music:
main_ambient: Rain_and_thunder.ogg
...
// Shaders:
card: card.vs, card.fs
skybox: -, skybox.fs
...
I then added a step to my build.jai
script during which I parse this file, verify that every defined asset exists and generate code which allows me to use those assets in game.
For instance, when parsed the file above will generate the following enum for texture assets:
Texture_Asset : : enum u8 {
metal;
wood;
brick;
glass;
}
The type of an enum will change automatically to accommodate the number of assets of specific type, in practice this means enum will either be u8
or u16
. Because I don't have the budget or need more than 65k
assets of any given type, or in total.
Along with enums, the build will also generate functions to load and get texture assets:
load_texture : : inline (asset_key: Texture_Asset) {
textures[asset_key] = load_texture_from_disk(texture_sources[asset_key].src);
}
get_texture : : inline (asset_key: Texture_Asset) - > Texture_Id {
return textures[asset_key];
}
Finally, a function which initializes all the assets is also generated. This function is invoked at the start and it's inexpensive since it only initializes arrays which contain sources and configuration of any given asset.
init_assets : : () {
texture_sources[0 ] = .{ src= "../assets/textures/Metal128.png" };
texture_sources[1 ] = .{ src= "../assets/textures/Light_wood.png" };
texture_sources[2 ] = .{ src= "../assets/textures/Brick_wall.png" };
texture_sources[3 ] = .{ src= "../assets/textures/Glass.png" };
// init other asset sources ...
};
The expensive part is the loading via: load_texture, load_sound, etc.
functions and this is done on demand. In cases where an asset is not loaded the id will be equal to 0
and it refers to the fallback asset which is meant to signal that some asset either failed to load or is still loading.
All of this code is generated really quickly so compile time is unaffected. But, since I'm already here I decided to make the generator step run only if assets.ad
file changes. To accomplish this I made the Watchdog utility pass a special argument: "-gen_assets"
to the build script when assets.ad
file changes.
@echo off
start cmd.exe /k Watchdog.exe ^
delay_ms="32" ^
working_directory="C:/Puhomir/Projects/Oil_Rig/" ^
-dirs ^
src/ ^
modules/ ^
build/
-ignore ^
build_generated/ ^
-command ^
scripts/build.bat
-on_change ^
assets.ad ^
-gen_assets
This is the build script which is called by watchdog:
@echo off
jai build/build.jai -import_dir "../modules" -quiet - %1
On the build.jai
side I just check if "-gen_assets"
argument is present in the compile time command line arguments array. If found, I call the asset definition generator for that workspace, which will then generate all the code we've already seen and add it to the project build.
Using assets
Finally here's how the generated code is used:
model : = Basic_Model.{
mesh= get_mesh(.table),
texture= get_texture(.wood)
};
play_music(.main_ambient);
I really like this, it's simple and my compiler will help with a lot of common mistakes I would otherwise make with strings. If I mistype the name, compiler will complain, if a name does not exist for this specific asset type the compiler will complain. It's much more foolproof than strings, and I find it easier to type.
Disadvantage of this approach is that my compiler will complain, it may be the case that at larger scale this makes development more difficult. If it becomes troublesome I will change it back to strings and use some metaprogram to remap them to enums for release builds.