Handle Asset Loading With Annotations

3 minute read

The AssetManager handles loading and unloading various assets such as Textures and Sounds. Rather than loading them on use, you can use the Asset Manager to load all necessary assets at game launch, or at the beginning of a level. It also serves as a central point for all assets, making it easier to avoid reimporting the same asset multiple times.

val manager = AssetManager()
manager.load("data/mytexture.png", Texture::class.java)
/*
 * Load all assets that we called 'load' on.
 * As long as update returns false, we havn't finished loading
 */
while (!manager.update()) {
  // do nothing
}
// Use the asset
val texture: Texture = manager.get("data/mytexture.png", Texture::class.java);

While using the Asset Manager is much better than loading them on the fly, managing the load and get calls can very quickly start to get messy, not to mention all of the redundant code. Ideally, we would like to declare an asset once, and have the engine handle the loading and whatnot for us.

Proposed solution

@Asset
val textureAsset = AssetDescriptor("data/myTexture.png", Texture::class.java)

val texture: Texture = texture.get()

We’ve introduced the @Asset annotation, which will let us tell our engine that the following field is an asset to be loaded by our asset manager. Here is what the annotation declaration looks like.

@Target(AnnotationTarget.FIELD)
annotation class Asset()

By using this simple annotation we’ve done two things. We avoid having to drag along our asset manager wherever we want to declare assets, and we’ve created one single reference to handle both telling our asset manager to load the object and also serving as a reference point to obtain the actual asset.

However, simply adding the annotation won’t put everything into motion. Let’s see how we actually load the assets at startup.

Loading the Assets

Now that we’ve annotated our fields, we need to fetch this information and queue up the loading. Let’s take a look.

private fun loadAllAssets() {
  val reflection = MainContext.inject<Reflections>()
  val assets = reflection
  .getFieldsAnnotatedWith(Asset::class.java)
  .mapNotNull {
    it.isAccessible = true // Workaround for having this work despite wanting to limit scope of certain assets
    it.get(null) as? AssetDescriptor<*>
  }
  
  assets.forEach {
    assetManager.load(it)
  }
}

This basically replaces all of our load calls from the initial method. We’re using the org.reflections:reflections library to fetch all fields with the @Asset annotation as AssetDescriptors and loading them all. Although I haven’t talked about it so far, AssetDescription is an existing LibGDX class which basically holds the path and filetype information of an asset.

Once we’ve queued up the assets, we need to start loading them all. This hasn’t changed from before so we simply call assetManager.update() each frame, which returns true if we’re finished loading everything.

Obtaining the Assets

The final piece of the puzzle is the get extension function, which lets us obtain the actual asset via the same reference with annotated earlier.

fun <T> AssetDescriptor<T>.get(): T {
    return assetManager.get(this)
}

Using generics lets us omit the actual class name when permitted, which is useful in keeping things short and concise.

Putting it all together

Let’s look at how it all comes together. For reuse in all my projects, I’ve wrapped everything found here in my own AssetManager class. Here is what it looks like (certain lines omited or changed for brevity and completeness).

// package ...
import com.badlogic.gdx.assets.AssetDescriptor
import ktx.freetype.registerFreeTypeFontLoaders
import org.reflections.Reflections
import com.badlogic.gdx.assets.AssetManager as GdxAssetManager

class AssetManager {

    init {
        assetManager.registerFreeTypeFontLoaders()
        loadAllAssets()
    }

    fun install() {
        assetManager.registerFreeTypeFontLoaders()
        loadAllAssets()
    }

    fun update(): Boolean {
        return assetManager.update()
    }

    fun progress(): Float {
        return assetManager.progress
    }

    private fun loadAllAssets() {
        val reflection = Reflections("com.xanadu.section", FieldAnnotationsScanner(), TypeAnnotationsScanner(), SubTypesScanner())
        val assets = reflection
                .getFieldsAnnotatedWith(Asset::class.java)
                .mapNotNull {
                    it.isAccessible = true
                    it.get(null) as? AssetDescriptor<*>
                }
        assets.forEach {
            assetManager.load(it)
        }
    }

    companion object {
        private val assetManager: GdxAssetManager = GdxAssetManager()
        
        fun <T> AssetDescriptor<T>.get(): T {
            return assetManager.get(this)
        }
    }
}

@Target(AnnotationTarget.FIELD)
annotation class Asset()

Improvements for the Future

  • Right now we’re using reflection at runtime to obtain all the assets. Ideally we should be generating this code with something like Kotlin Poet at compile time, as bigger projects will take some time finding all of our annotated fields.
  • Our @Asset annotation is very simple, we could enrich it with some helpful functionalities. For example, we could add a Level enum to denote which levels of our game uses this asset, so instead of loading all assets at the beginning, we could load necessary assets at the beginning of each level, to avoid a large load time at startup.