Loading assets asynchronously with LibGDX AssetManager
Learn how to queue textures, track dependencies, and safely retrieve assets using LibGDX's AssetManager without blocking the game loop.
07 Dec 2025, 00:28 UTC

Using LibGDX AssetManager for asynchronous asset loading
When a game needs to load textures, sounds, models or other resources, doing so synchronously can stall the render thread and cause noticeable frame drops. LibGDX provides AssetManager to move the actual I/O work to a background thread while the main loop continues to run. The manager reports progress each frame, letting you show a loading screen or simply wait until update() returns true before using the asset.
Typical workflow
- Create a single
AssetManagerinstance (usually in yourScreenorGameclass). - Call
load(fileName, type)for each resource you need. - In the render loop invoke
manager.update(). This method returnsfalsewhile loading is in progress andtruewhen all queued assets have finished. - Once
update()returnstrue, retrieve the asset withmanager.get(fileName, type). - When the screen is disposed, call
manager.dispose()to free native resources.
Worked example
public class PlayScreen implements Screen {
private final AssetManager assets = new AssetManager();
private Texture playerTex;
private boolean ready = false;
@Override
public void show() {
// Queue a texture and a texture atlas (with dependency example)
assets.load("data/player.png", Texture.class);
assets.load("data/game.atlas", TextureAtlas.class);
// Suppose you need a region from the atlas later; you can express the dependency:
assets.load("data/game.atlas", TextureAtlas.class,
new TextureAtlasLoader.Parameter()); // same type, just to illustrate
// In practice you would load the atlas first, then later request a region:
// assets.load("data/game.atlas", TextureAtlas.class);
// assets.load("data/player_region.png", Texture.class,
// new AssetLoaderParameters() {
// { dependencies = new Array();
// dependencies.add(new AssetDescriptor("data/game.atlas", TextureAtlas.class));
// }
// });
}
@Override
public void render(float delta) {
// Advance loading; do this every frame.
if (!ready && assets.update()) {
ready = true;
playerTex = assets.get("data/player.png", Texture.class);
// If you used the dependency example, you could now safely get the region:
// TextureRegion region = assets.get("data/game.atlas", TextureAtlas.class)
// .findRegion("player");
}
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
if (playerTex != null) {
// draw the texture (batch.begin(), batch.draw(...), batch.end())
}
// Optionally show a loading percentage:
// float progress = assets.getProgress();
}
@Override
public void resize(int width, int height) { }
@Override
public void pause() { }
@Override
public void resume() { }
@Override
public void hide() { }
@Override
public void dispose() {
// Dispose only after you are sure no other part of the game holds references.
assets.dispose();
}
}
How the mechanism works
Internally, AssetManager maintains a queue of AssetDescriptor objects. When load() is called, a descriptor is added but no I/O occurs yet. Each frame, update() pulls the next descriptor from the queue and delegates the actual loading to the appropriate AssetLoader (e.g., TextureLoader, SoundLoader, ModelLoader) which runs on a separate thread managed by AsyncExecutor. While the loader is working, the render thread continues; update() only returns true when every queued loader has signaled completion.
Dependency tracking lets you express that one asset cannot be used before another is ready. By adding an AssetLoaderParameters with a list of dependencies, the manager will not mark the dependent asset as finished until all its prerequisites have completed. This prevents situations where a sprite sheet texture is accessed before its atlas has been fully uploaded to the GPU.
Limits and considerations
- Single manager per context. While you can create multiple
AssetManagerinstances, doing so duplicates the internal thread pool and can waste resources. A common practice is to keep one manager perGameor perScreenthat lives for the lifetime of that screen. - Update frequency matters. If you call
update()only occasionally (e.g., once per second), large assets may take many seconds to finish because the loader only makes progress when the method is invoked. The typical pattern is to call it every frame insiderender(). - Network or slow disk. The manager does not magically speed up I/O; loading from a network source or a slow storage device will still take time. You should still show a loading indicator based on
assets.getProgress(). - Error handling. By default, a failed load logs a warning and the asset is considered missing. You can attach an
AssetErrorListenerto the manager to react to failures (e.g., show a fallback texture). - Memory usage. Assets remain in memory until you call
dispose(). Holding onto textures you no longer need can cause out‑of‑memory errors, especially on mobile.
Common mistakes
- Calling
get()beforeupdate()reports completion. This returnsnull(or a placeholder if you configured one) and leads toNullPointerExceptionwhen you try to draw the texture. Always guard the retrieval with theupdate()return value or checkisLoaded(assetDescriptor). - Disposing while assets are still in use. If you call
assets.dispose()while a sprite batch still holds a texture reference, the native OpenGL texture is deleted and subsequent draw calls produce undefined behavior (often a black rectangle or crash). Ensure all references are cleared or the screen is fully torn down before disposing. - Failing to call
update()each frame. Some developers place the call inside a loading screen that is removed too early, leaving the manager stuck at99%progress. Keep the call in the active screen’srender()method untilupdate()returnstrue. - Misunderstanding dependency ordering. Adding a dependency after the dependent asset has already been queued has no effect; the manager evaluates dependencies at the moment the descriptor is added. Define dependencies before loading the dependent asset, or reload the manager.
Practical verification steps
- Generate a minimal LibGDX project via the gdx‑setup UI or Gradle.
- Place a PNG file named
player.pnginside theassets/data/folder. - Insert the code snippet above into a new
Screenimplementation. - Run the desktop launcher. Observe that the texture does not appear immediately; the console will show debug output from
AssetManager(if you enable logging) indicating loading progress. - After a few frames,
update()returnstrueand the texture is rendered. - To confirm the caution, temporarily move the line
playerTex = assets.get("data/player.png", Texture.class);before theif (!ready && assets.update())block and run again. The texture will benulland attempting to draw it will produce no image or a warning in the log.
By following this pattern you can keep your game loop responsive while assets are streamed in, safely handle dependencies, and avoid the most frequent pitfalls associated with LibGDX’s asynchronous loading system.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.