Utilities for management of assets and heavy resources.
While libGDX does a good job of helping you with assets through AssetManager
and related APIs, is does not use
the full potential of the Kotlin features, especially when it comes to handling Class
instances. This library
aims to provide Kotlin extensions and wrappers for the existing asset APIs to make assets usage more idiomatic
in Kotlin applications.
AssetManager.load
extension method can be used to schedule asynchronous loading of an asset. It returns an asset wrapper, which can be used as delegate property, as well as used directly to manage the asset. Usually the asset will not be available untilAssetManager.finishLoading
or loopedAssetManager.update
are called. You can use string file paths orAssetDescriptor
instances to load the asset. Usage example:
// Eagerly loading an asset:
val wrapper = load<Texture>("test.png")
wrapper.finishLoading()
val texture = wrapper.asset
// Delegate field:
class Test(assetManager: AssetManager) {
val texture by assetManager.load<Texture>("test.png")
// Type of texture property is Texture.
}
AssetManager.getAsset
utility extension method can be used to access an already loaded asset, without having to pass class to the manager to specify asset type. This is the preferred way of accessing assets from theAssetManager
, provided that they were already scheduled for asynchronous loading and fully loaded. Note that this method will fail if asset is not loaded yet. Usage example:
val texture: Texture = assetManager.getAsset("test.png")
AssetManager.loadOnDemand
is similar to theload
utility method, but it provides an asset wrapper that loads the asset eagerly on first get call. It will not schedule the asset for asynchronous loading - instead, it will block current thread until the asset is loaded on the first access. Use for lightweight assets that should be (rarely) loaded only when requested. Usage example:
// Eagerly loading an asset:
val texture by assetManager.loadOnDemand<Texture>("test.png")
// Asset will be loaded upon first `texture` usage.
// Delegate field:
class Test(assetManager: AssetManager) {
val texture: Texture by assetManager.loadOnDemand("test.png")
// Asset will be loaded on first `texture` access.
}
AssetManager.unloadSafely
is a utility method that attempts to unload an asset from theAssetManager
. Contrary toAssetManager.unload
call, this is a graceful method that will not throw any exceptions if the asset was not even loaded in the first place. Typical usage:assetManager.unloadSafely("test.png")
.AssetManager.unload
extension method consuming a exception handling block was added, so you can gracefully handle exception thrown during reloading. Note thatAssetManager
can throwGdxRuntimeException
if the asset was not loaded yet.AssetManager.getLoader
andsetLoader
extension methods with reified types added to ease handling ofAssetLoader
instances registered in theAssetManager
.- The
AssetGroup
class is provided for easily grouping together assets so they can be managed as a group through calls such asloadAll()
orunloadAll()
. The intended use is to subclassAssetGroup
and list its member assets as properties usingAssetGroup.asset()
orAssetGroup.delayedAsset()
. It also allows for using a common prefix for the file names of the group in case they are stored in a specific subdirectory.
- Null-safe
Disposable.disposeSafely()
method was added. Can be called on a nullableDisposable?
variable. Ignores most thrown exceptions (except for internal JVMError
instances, which should not be caught anyway). Disposable.dispose
with an exception catch block was added. Usingasset.dispose { exception -> doSomething() }
syntax, you can omit a rather verbose try-catch block and handle exceptions with a Kotlin lambda.- Any
Iterable
orArray
storingDisposable
instances will havedispose
,dispose { exception -> }
anddisposeSafely
methods that dispose stored assets ignoring anynull
elements. This is a utility for disposing collections of assets en masse. - All exceptions get a utility
ignore()
method that you can switch at compile time (for debugging or logging) when needed. SeeThrowable.ignore()
documentation for further details.
DisposableContainer
is a Disposable
implementation that stores a set of Disposable
instances to be disposed
all at once.
When subclassed or used as a delegate via its DisposableRegistry
interface, it provides an alsoRegister
extension,
which allows to easily add items to the container so that they will be automatically disposed when the containing class is.
Pool
instances can be invoked like a function to provide new instances of objects. Basically, this syntax:pool()
has the same effect as directly callingpool.obtain()
.Pool
instances can be invoked like a one argument function to free instances of objects. Basically, this syntax:pool(instance)
has the same effect as directly callingpool.free(instance)
.- New instances of
Pool
can be easily created with Kotlin lambda syntax usingpool
method. For example, this pool would return new instances ofEntity
once empty:pool { Entity() }
. Since this method is inlined, you should not worry about unnecessary extra method calls or extra objects - thePool
implementations are prepared at compile time.
- Any Kotlin string can be quickly converted to a
FileHandle
instance usingtoClasspathFile
,toInternalFile
,toLocalFile
,toExternalFile
ortoAbsoluteFile
. This is basically a utility for accessingGdx.files.getFileHandle
method with a pleasant Kotlin syntax. file
utility function allows to quickly obtain aFileHandle
instance. It features an optionaltype
parameter which allows to choose theFileType
, while defaulting to the most commonInternal
.
FileType.getResolver
extension method was added to quickly constructFileHandleResolver
instances for the chosen file types.FileHandleResolver.withPrefix
extension method was added to ease decoration ofFileHandleResolver
instances withPrefixFileHandleResolver
.FileHandleResolver.forResolutions
extension method was added to ease decoration ofFileHandleResolver
instances withResolutionFileResolver
.resolution
factory function was added to constructResolutionFileResolver.Resolution
instances with idiomatic Kotlin syntax.
TextAssetLoader
allows to read text files asynchronously via theAssetManager
. Very useful if you want to leverageAssetManager
loading and management API for a notable number of text files.
Obtaining FileHandle
instances:
import com.badlogic.gdx.Files.FileType.*
import ktx.assets.*
val fileHandle = "my/file.png".toInternalFile()
val internal = file("my/file.png")
val absolute = file("/home/ktx/my/file.png", type = Absolute)
Working with libGDX Pool
:
import ktx.assets.*
val pool = pool { "String." }
val obtained: String = pool() // "String."
pool(obtained) // Returned instance to the pool.
Gracefully disposing assets:
import ktx.assets.*
texture.disposeSafely()
music.dispose { exception ->
println(exception.message)
}
Disposing collections of assets:
import ktx.assets.*
val textures: Array<Texture> = getMyTextures() // Works with any Iterable, too!
textures.dispose() // Throws exceptions.
textures.disposeSafely() // Ignores exceptions.
textures.dispose { exception -> } // Allows to handle exceptions.
Registering Disposable
instances for disposal when the containing class is disposed:
import com.badlogic.gdx.Screen
import com.badlogic.gdx.assets.AssetManager
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import ktx.assets.*
class MyScreen: Screen, DisposableRegistry by DisposableContainer() {
val assetManager = AssetManager().alsoRegister()
val spriteBatch = SpriteBatch().alsoRegister()
}
Manually disposing registered Disposable
instances with custom error handling:
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.ScreenAdapter
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import ktx.assets.*
class MyScreen: ScreenAdapter(), DisposableRegistry by DisposableContainer() {
val spriteBatch = SpriteBatch().alsoRegister()
override fun dispose() {
// DisposableRegistry offers registeredDisposables list
// that can be accessed to perform custom disposal:
registeredDisposables.dispose { exception ->
Gdx.app.error("MyScreen.dispose()", exception.message)
}
}
}
Resolving ambiguity given multiple dispose
implementations - storing a reference to DisposableContainer
to
call its dispose
method explicitly:
import com.badlogic.gdx.ScreenAdapter
import ktx.assets.*
class MyScreen(
private val registry: DisposableRegistry = DisposableContainer()
): ScreenAdapter(), DisposableRegistry by registry {
// `dispose` has to be overridden, as it is provided by both
// DisposableRegistry and ScreenAdapter:
override fun dispose() {
// Calling registry explicitly:
registry.dispose()
}
}
Scheduling assets loading by an AssetManager
:
import ktx.assets.*
assetManager.load<Texture>("image.png")
Using field delegate which will eventually point to a Texture
(after it's fully loaded by an AssetManager
):
import ktx.assets.*
import com.badlogic.gdx.assets.AssetManager
class MyClass(assetManager: AssetManager) {
val image by assetManager.load<Texture>("image.png")
// image is Texture == true
}
Immediately extracting a fully loaded asset from an AssetManager
:
import ktx.assets.*
val texture: Texture = assetManager.getAsset("image.png")
// Or alternatively:
val texture = assetManager.getAsset<Texture>("image.png")
Using an asset loaded on the first getter call rather than scheduled for loading:
import ktx.assets.*
import com.badlogic.gdx.assets.AssetManager
class MyClass(assetManager: AssetManager) {
val loadedOnlyWhenNeeded by assetManager.loadOnDemand<Texture>("image.png")
// loadedOnlyWhenNeeded is Texture == true
}
Unloading an asset from an AssetManager
, ignoring exceptions:
import ktx.assets.*
assetManager.unloadSafely("image.png")
Unloading an asset from an AssetManager
, handling exceptions:
import ktx.assets.*
assetManager.unload("image.png") { exception -> exception.printStackTrace() }
Managing AssetLoader
instances of an AssetManager
:
import ktx.assets.*
// Settings custom AssetLoader:
val myCustomLoader = CustomLoader()
assetManager.setLoader(myCustomLoader) // No need to pass class.
// Accessing an AssetLoader:
val loader = assetManager.getLoader<MyAsset>()
Creating an InternalFileHandleResolver
:
import ktx.assets.getResolver
import com.badlogic.gdx.Files.FileType
val resolver = FileType.Internal.getResolver()
Decorating FileHandleResolver
with PrefixFileHandleResolver
:
import ktx.assets.*
import com.badlogic.gdx.Files.FileType
val resolver = FileType.Internal.getResolver().withPrefix("folder/")
Decorating FileHandleResolver
with ResolutionFileResolver
:
import ktx.assets.*
import com.badlogic.gdx.Files.FileType
val resolver = FileType.Internal.getResolver().forResolutions(
resolution(width = 800, height = 600),
resolution(width = 1024, height = 768)
)
A simple application that registers TextAssetLoader
, reads a file asynchronously, prints it and terminates:
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.assets.AssetManager
import ktx.assets.TextAssetLoader
import ktx.assets.load
import ktx.assets.setLoader
class MyApp : ApplicationAdapter() {
private val manager = AssetManager()
override fun create() {
manager.setLoader(TextAssetLoader())
manager.load<String>("file.txt")
}
override fun render() {
if (!manager.update()) {
println("Loading...")
} else {
println(manager.get<String>("file.txt"))
Gdx.app.exit()
}
}
}
Using AssetGroup
to schedule loading and manage a group of assets:
/** Groups UI-related assets. */
class UIAssets(manager: AssetManager) : AssetGroup(manager, filePrefix = "ui/") {
// The assets are listed using asset(),
// so they are immediately queued for loading when the object is created.
val skin by asset<Skin>("skin.json")
val clickSound by asset<Sound>("mapScreen.wav")
}
val uiAssets = UIAssets(manager)
// No need to queue the assets. They are queued when creating the group object.
// Block until they are finished loading:
uiAssets.finishLoading()
// Note that classic incremental loading with update() is also available.
// Accessing assets - same as with regular properties:
uiAssets.skin
uiAssets.clickSound
// Disposing of the assets:
uiAssets.unloadAll()
Using AssetGroup
with assets loaded on demand once needed:
/** An asset groups that loads the assets on demand on first access. */
class MapScreenAssets(manager: AssetManager) : AssetGroup(manager, filePrefix = "mapScreen/") {
// The assets are listed using delayedAsset(),
// so they are loaded on demand on the first access
// or have to be scheduled for loading manually with loadAll().
val atlas by delayedAsset<TextureAtlas>("map.atlas")
val music by delayedAsset<Music>("mapScreen.ogg")
}
val mapScreenAssets = MapScreenAssets(manager)
// Assets are not queued for loading just yet.
// To load the assets, you can access them with properties:
mapScreenAssets.atlas
// Note that this will block until the asset is loaded.
// You can schedule them for asynchronous loading to avoid thread blocking:
mapScreenAssets.loadAll()
if (mapScreenAssets.update()) {
// The member assets are now ready to use.
} else {
// Continue showing loading prompt.
}
// Disposing of the assets:
mapScreenAssets.unloadAll()
Create an enum with all assets of the selected type. Let's assume our application stores all images in assets/images
folder in PNG
format. Given logo.png
, player.png
and enemy.png
images, we would create a similar enum:
enum class Images {
logo,
player,
enemy;
val path = "images/${name}.png"
fun load() = manager.load<Texture>(path)
operator fun invoke() = manager.getAsset<Texture>(path)
companion object {
lateinit var manager: AssetManager
}
}
Operator invoke()
function brings asset accessing boilerplate to mininum: enumName()
. Thanks to wildcard imports, we
can access logo
, player
and enemy
enum instances directly:
import com.example.Images.*
// Setting AssetManager instance:
Images.manager = myAssetManager
// Scheduling logo loading:
logo.load()
// Getting Texture instance of loaded logo:
val texture = logo()
// Scheduling loading of all assets:
Images.values().forEach { it.load() }
// Accessing all textures:
val textures = Images.values().map { it() }
ktx-assets-async
provides an alternative asset manager with non-blocking API based on coroutines. In contrary to libGDXAssetManager
, KTXAssetStorage
supports concurrent loading of assets on multiple threads performing asynchronous operations.- libgdx-utils feature an annotation-based asset manager implementation which easies loading of assets (through internal reflection usage).
- Autumn MVC is a Spring-inspired model-view-controller framework built on top of libGDX. It features its own asset management module which loads and injects assets into annotated fields thanks to reflection.
- Kiwi library has some utilities for assets handling, like
graceful
Disposable
destruction methods and libGDX collections implementingDisposable
interface. It is aimed at Java applications though - KTX syntax should feel more natural when using Kotlin.