Eliminate Frame Stalls with libGDX AssetManager
Learn how libGDX’s AssetManager loads assets asynchronously, manages references, and keeps your game running smoothly. A practical example, trade‑offs, and best‑practice checklist are included.
08 Sept 2025, 11:35 UTC

Problem: Frame Stalls When Loading Assets
In a typical libGDX game, calling new Texture("/path.png") or new Sound("/path.wav") blocks the rendering thread until the file is read, decoded, and uploaded to the GPU. When a level change or a menu opens, the player sees a freeze, and the FPS drops to 0 for a frame or two. This is especially noticeable on mobile devices with limited CPU and memory bandwidth.
Thesis: Use AssetManager for Asynchronous Loading
The AssetManager class is libGDX’s built‑in solution for loading resources in the background. It queues load requests, runs them on a worker thread, and exposes an update() method that you call each frame to process the queue. When all requested assets are ready, update() returns true and you can safely retrieve them with get().
Key Features Explained
Background Thread Loading
When you call load() on the manager, the asset is added to an internal queue. The manager owns a single background thread that reads the file, decodes it, and uploads it to the GPU. The main thread can continue rendering while this happens.
Reference Counting
Each asset has a reference counter. Every time you call load() for the same asset, the counter increments. When you call unload(), the counter decrements. The asset is only freed when the counter reaches zero, preventing accidental disposals while still in use.
Dependency Resolution
Complex assets like TextureAtlas reference individual Texture files. The manager automatically loads the dependencies first, so you never retrieve an incomplete atlas.
Error Handling
Implement AssetManager.setErrorListener() to catch missing or corrupt files. The listener receives the asset key and the exception, allowing you to substitute a placeholder texture or play a default sound.
Concrete Example: Loading a Level with Textures and Sounds
- Initialize the manager (main thread):
public class MyGame extends Game { public AssetManager assetManager; @Override public void create() { assetManager = new AssetManager(); assetManager.setErrorListener((key, file, exception) -> { Gdx.app.error("AssetManager", "Failed to load: " + key, exception); // Fallback: load a default texture if (key.endsWith(".png")) { assetManager.load("fallback.png", Texture.class); } }); } }- Queue assets at level start (main thread):
// Called when entering a new level private void loadLevelAssets() { assetManager.load("level1/tiles.png", Texture.class); assetManager.load("level1/tiles.atlas", TextureAtlas.class); assetManager.load("level1/ambient.wav", Sound.class); // Optionally start a loading screen }- Progress in render loop (main thread):
public void render() { if (!assetManager.update()) { // Still loading: show progress bar float progress = assetManager.getProgress(); // render loading UI return; } // All assets ready: start the level Texture tiles = assetManager.get("level1/tiles.png", Texture.class); TextureAtlas atlas = assetManager.get("level1/tiles.atlas", TextureAtlas.class); Sound ambient = assetManager.get("level1/ambient.wav", Sound.class); // continue normal rendering }- Unload when leaving the level (main thread):
private void unloadLevelAssets() { assetManager.unload("level1/tiles.png"); assetManager.unload("level1/tiles.atlas"); assetManager.unload("level1/ambient.wav"); }
Notice that all load(), unload(), and get() calls happen on the main thread. The heavy lifting is done in the worker thread, which keeps the FPS stable.
Trade‑Offs and Limitations
- Single‑Threaded API Calls: The manager’s public methods must be invoked from the rendering thread. Calling
load()from a background thread can lead to race conditions and crashes. - Memory Footprint: While assets are queued, the manager keeps them in memory until
finishLoading()orupdate()completes. For very large levels, consider streaming assets or using a smaller texture atlas. - Initial Block on First Load: If you call
finishLoading()duringcreate(), the main thread will block until all assets are ready. Useload()+update()instead to keep the splash screen responsive.
Practical Checklist Before Release
- Verify that every
load()call is paired with a correspondingunload()or that the asset is kept for the entire game session. - Run a memory profiler (Android Studio, VisualVM) to confirm that the heap usage does not spike during level transitions.
- Enable the error listener and intentionally omit a texture file to ensure the fallback mechanism triggers.
- Measure FPS on target devices with and without
AssetManagerto confirm the performance benefit. - Check the reference count for a reused asset (e.g., a shared UI texture) to ensure it isn’t unloaded prematurely.
Actionable Closing
Switching to AssetManager for all runtime asset loading removes frame stalls, automatically manages memory, and gives you a single place to handle load failures. Start by wrapping your existing new Texture() calls in assetManager.load(), move the retrieval to the render() loop, and add an error listener. The result is a smoother player experience and a cleaner asset lifecycle management.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.