LibGDX AssetManager: Asynchronous Loading, Reference‑Counting, and Hot‑Reload Architecture
LibGDX AssetManager lets you load textures, sounds, and custom data asynchronously while keeping reference‑counted disposal and hot‑reload ready. This guide walks through the minimal architecture, operational checks, failure modes, and when to adjust the design for streaming or WebGL targets.
29 Oct 2025, 09:43 UTC

Problem & Takeaway
When building a LibGDX game, you often need to load dozens of textures, sounds, and custom data without stalling the render loop. AssetManager solves this with asynchronous queues, reference‑counted disposal, and a hot‑reload path. The key takeaway is that a minimal, safe design uses a single AssetManager instance, enqueues assets only from the main thread, and relies on manager.update() for progress polling. This guarantees that assets are shared correctly, GPU resources are freed only when no screen uses them, and mods or asset changes can be applied at runtime.
Requirements
- LibGDX 1.12+ (Android, desktop, iOS, WebGL)
- Single
AssetManagerper game or per screen lifecycle if isolation is required. - All
load()calls executed on the rendering thread (or synchronized). - Custom loaders must not touch OpenGL from background threads.
- For hot‑reload, the same
AssetDescriptorkey must be reused.
Smallest Suitable Design
Below is a concise pattern that satisfies the above constraints.
public class MyGame extends Game {
private AssetManager assetManager = new AssetManager();
@Override
public void create() {
// Queue assets
assetManager.load("textures/player.png", Texture.class);
assetManager.load("sounds/jump.wav", Sound.class);
assetManager.load("maps/level1.json", TiledMap.class);
}
@Override
public void render() {
if (!assetManager.update()) {
float progress = assetManager.getProgress();
// draw progress bar using progress
} else {
if (currentScreen == null) setScreen(new MainMenuScreen(assetManager));
}
}
}
In this skeleton:
- All assets are queued once in
create(). - The render loop polls
update()until the queue is empty. - Once loaded, a screen is instantiated with the shared manager.
Trust & Data Boundaries
AssetManager keeps a reference map: Map<String, Integer> referenceCounts. When load() is called, the count increments; unload() decrements. The native resource is freed only when the count reaches zero. This guarantees:
- Two screens can share the same texture without duplication.
- Disposing a screen that still uses the texture does not crash the GPU.
- Assets loaded via a custom loader (e.g.,
JsonLoader) follow the same rules.
Operational Checks
manager.update()returnstruewhen the queue is drained.manager.getProgress()yields a float 0–1 for UI feedback.manager.getLoadedAssets()andmanager.getQueuedAssets()list asset names for debugging.- Enable verbose logging:
manager.setLogger(new Logger("AssetManager", Logger.DEBUG)); - After a screen finishes, call
manager.unload("textures/player.png");only if you know no other screen references it.
Failure Modes & Mitigations
| Failure | Cause | Mitigation |
|---|---|---|
| Out‑of‑Memory (OOM) | Large textures uploaded without mipmaps or wrong format. | Use Pixmap.setFilter() and Pixmap.setFormat() before loading; pack textures into atlases. |
| Missing File | AssetDescriptor points to a nonexistent file. | Error only thrown on update(); catch AssetLoadException and provide fallback. |
| Cross‑thread GL Calls | Custom loader touches GL on the background thread. | Queue GL work via Gdx.app.postRunnable() or implement Disposable with reload(). |
| Premature Disposal | Manager disposed while screens still use assets. | Ensure all screens call manager.unload() before manager.dispose() or use a global shutdown hook. |
| Context Loss (Android) | Screen rotation or low‑memory kill clears GL objects. | Call manager.reload() after Gdx.app.getApplicationListener().resume() and implement Disposable.reload() for custom shaders. |
Hot‑Reload Path
Hot‑reload is useful for desktop testing or mod support. The pattern is:
- Keep the original
AssetDescriptorunchanged. - Replace the underlying
FileHandleby callingAssetManager.reload(descriptor). - The manager will re‑queue the asset, increment the reference count, and replace the internal object once loading completes.
Example:
AssetDescriptor<Texture> texDesc =
new AssetDescriptor<>("textures/player.png", Texture.class);
assetManager.reload(texDesc); // triggers hot‑reload
After update() returns true, the sprite using this texture automatically displays the new image.
Custom Loader Example
Suppose you need to load a Spine skeleton. You create a loader that parses the JSON and returns a SkeletonData object. The loader must not touch GL on its background thread.
public class SkeletonLoader extends BaseAssetLoader<SkeletonData, SkeletonDescriptor> {
public SkeletonLoader(FileHandleResolver resolver) { super(resolver); }
@Override
public SkeletonData load(AssetManager manager, String fileName, FileHandle file,
SkeletonDescriptor parameter, Map<Object, Object> attachments) {
// parse JSON, create SkeletonData
return new SkeletonData();
}
}
Register it in create():
assetManager.setLoader(SkeletonData.class,
new SkeletonLoader(Gdx.files.classpath("assets/")));
assetManager.load("skeletons/hero.json", SkeletonData.class);
Design Changes When Conditions Shift
- Streaming worlds: Replace the default loader with a
StreamingTextureLoaderthat loads tiles on demand and keeps a sliding window of references. - Deterministic load order: For networked multiplayer, use synchronous
load()calls andmanager.finishLoading()before sending state to peers. - WebGL target: WebGL cannot use background decoding; branch on
Gdx.app.getType()and load synchronously or useGdx.net.sendHttpRequest()to fetch assets.
Practical Verification Checklist
- Run
manager.isLoaded("textures/player.png")afterupdate()to confirm the asset is ready. - Use
manager.getProgress()in a debug overlay to watch loading. - Profile native heap before and after
manager.dispose()to ensure no leaks. - On Android, rotate the device and verify textures reappear; log
manager.getLoadedAssets()post‑resume. - During hot‑reload, check that the sprite’s texture region changes without restarting the game.
Conclusion
By treating AssetManager as a single source of truth for all resources, you gain predictable memory usage, safe sharing across screens, and a clean hot‑reload mechanism. Keep all load/unload calls on the main thread, respect reference counts, and use the provided operational checks to catch errors early. When your game’s architecture evolves—streaming, deterministic sync, or WebGL—adapt the loader strategy rather than overhauling the manager itself.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.