wiltshire.sh

improving my game engine's asset manager

introduction

hi! so, i’ve been writing this game engine for almost a year now, and i’m still not done even with the simplest things. and in order to progress further (to rendering, etc), i need to a little bit modify my asset server implementation.

what’s wrong with the current iteration?

i have not thought of loading assets automatically when initially writing the current version. i’ll try to explain some things in a simple way here, because i haven’t wrote any posts on the engine ever in that year :p.

when the asset server is initialized (e.g. with AssetServer::with_root("assets"), or AssetServer::new() - without a root directory, i’ll explain this), there’s nothing inside of it yet. whatever is using the asset server, the code must register its asset loaders (ErasedAssetLoader trait) and associate them with their respective asset types, e.g:

 asset_server.register_loader::<Audio>(Box::new(AudioLoader));

then load the assets:

 asset_server.load::<Audio>("music.ogg").unwrap();

simple, right? but what if we have dozens or hundreds of various assets, like an average game would? then we’d need to register the loaders, and then let the asset server load all these assets automatically? yes! but the issue is… the current asset server does not support this, and by “does not support” i mean - it CAN guess what loader to use for a file based on its extension (not implemented, but i have a draft snippet on that), but it CAN NOT load the asset, because… types. you can get a TypeId from a generic type in rust, but you can’t do the opposite - convert a TypeId into the type it describes.

and that is the issue.

what we need to do?

when i was writing the very first version of my engine, i used a manifest for the assets, e.g:

[[assets]]
name = "example"
path = "example.png"
type = "Image"

[[assets]]
name = "Terminus"
path = "terminus.ttf"
type = "Font"

[[assets]]
name = "tile_bg_empty"
path = "Sprite-0001.png"
type = "Image"

so simplifying, the asset manager would read the manifest -> match type (as they were built-in and not user-defined) -> load. what are we going to do? force the asset loaders to provide us with a unique name for them! so the manifest would explicitly specify what loader should the asset server use. let me show how i modified the trait:

/// Asset loader trait
pub trait ErasedAssetLoader: Send + Sync + Debug {
    /// Load the asset
    fn load_erased(&self, reader: &mut dyn Reader) -> ResourceResult<Box<dyn Any + Send + Sync>>;

    /// File extension(s) to automatically load if root dir is provided
    fn extensions(&self) -> &'static [&'static str] {
        &[]
    }

    // NEW!
    /// Unique name of the asset this loader handles. Used to automatically load assets by manifest.
    fn name(&self) -> &'static str;
}

simple, right? so now we need to make the asset server to read & respect the ErasedAssetLoader::name method. i’ve made a manifest for assets that looks like this:

Manifest(
    assets: [
        AssetDescriptor(
            name: "example",
            path: "images/example.png",
            kind: "Texture"
        )
    ]
)

the asset server would read the manifest, find the corresponding loader for an asset, and load it. and that is the hardest part. you see, the asset insertion process is heavily dependent on concrete types, you can’t just throw in a TypeId and forget about generics, they’re used a lot.

so… what?

well you see, inserting an asset requires acquiring the cache, which means:

  1. get a Box<dyn Any + Send + Sync> from self.caches which is a HashMap
  2. downcast the dyn Any to an AssetCache<A> how do we get rid of the A generic? make an ErasedCache trait with methods to_any, to_any_mut and insert_erased! then, the all remaining work is just make the existing code compatible with these changes.

phew! it was easier to explain, than to figure it out.. (took me 2 days).

further general improvements

now we have to improve the asset server code structure… i’ve been thinking of a builder pattern, so it’s the usual:

let asset_server = AssetServer::builer()
                    .with_root("assets/")
                    .with_loader::<Audio>(Box::new(AudioLoader))
                    .build();

it’s worth to note that:

  1. this part will be hidden from the end user of the engine, because we need the asset server to load scenes into the runtime
  2. i’ll keep an AssetServer::register_loader method if the user needs to register their own assets for some reason, but i’ll be sure to cover most of the usual needs: audio, textures, fonts, and some types like scripts

interested? see the code!

the source code of the engine is available here, and this specific module i’ve been writing about is here

thanks for reading!