Using LibGDX AssetManager for Safe, Asynchronous Resource Loading
Learn how AssetManager centralizes texture, sound, and font loading, tracks references, and lets you show a loading screen without stalling the render loop.
07 Aug 2025, 06:22 UTC

Why centralized loading matters
In a LibGDX game you often need textures, sounds, fonts, and custom data spread across many classes. Loading each resource directly where it is used leads to duplicated code, tight coupling, and the risk of loading the same asset twice. When loading happens on the main render thread a large file can stall the loop, causing a visible frame drop. A single, asynchronous loading point solves these problems by giving you one place to request assets, track when they are ready, and share them safely.
Setting up and using AssetManager
Create an AssetManager instance early, for example in your game's create() method. Register the asset types you need; LibGDX ships with loaders for common types such as Texture, Sound, Music, and BitmapFont, and you can add your own by implementing the AssetLoader interface. This is useful for proprietary formats or procedural content, and it does not require changing any existing loading code.
AssetManager assets = new AssetManager();
// Queue a texture and a sound for loading
assets.load("data/player.png", Texture.class);
assets.load("data/jump.wav", Sound.class);In your render() loop call assets.update(). This method performs any background loading that was queued and returns true when everything is finished. You can read assets.getProgress() (a float between 0 and 1) to draw a loading bar while you wait.
@Override
public void render(float delta) {
if (!assets.update()) {
// still loading
float progress = assets.getProgress();
drawLoadingBar(progress);
return;
}
// loading finished - now you can safely retrieve assets
Texture playerTex = assets.get("data/player.png", Texture.class);
Sound jumpSfx = assets.get("data/jump.wav", Sound.class);
// ...rest of your game logic...
}Because the actual I/O and decoding happen on a separate thread (when you use the default asynchronous loaders), the render loop continues at its target frame rate. When update() returns true and assets.isLoaded("data/player.png") is also true, the asset is ready for use.
Reference counting and sharing
AssetManager tracks a reference count for each loaded asset. If two screens or systems request the same texture, it is loaded once and shared; calling unload() only frees the underlying resource when the last reference is released. This makes it safe to pass the manager around instead of handing raw textures between classes. It also means you should treat unload() as releasing your claim, not necessarily destroying the asset immediately.
Trade-offs and practical verification
The main trade-off is that you must remember to dispose of the manager (or unload individual assets) when they are no longer needed. Forgetting to call assets.dispose() leaves GPU textures and audio buffers allocated, which on mobile can quickly exhaust limited memory. A simple way to verify correct disposal is to attempt to retrieve an asset after disposal; it should return null, and memory usage should drop accordingly.
Another limitation appears if you call assets.finishLoading() from the render thread. This blocks until all queued loads finish, which can cause a noticeable stall for large assets. The recommended pattern is to rely on update() each frame and only call finishLoading() during a dedicated loading screen or when you are certain the main thread is free.
To check that loading is truly asynchronous in a test project, load a texture with assets.load("data/my.png", Texture.class), then call assets.update() each frame in render() and confirm that assets.isLoaded("data/my.png") becomes true after a few frames without any frame drops. Watching the loader log messages can also confirm that background loading finished after update() returned true.
Actionable closing
Start by adding an AssetManager to your game's core class, queue all static resources in create(), drive the loading screen with assets.update() and assets.getProgress(), and dispose of the manager in dispose(). This gives you a centralized, non-blocking loading pipeline that scales from desktop to mobile while keeping your render loop smooth.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.