I was wondering if I am doing something wrong (most likely) or if there is an issue with LibGDX SkinLoader regarding additional resource dependencies.
According to the documentation of SkinLoader.SkinParameter you can pass an additional ObjectMap to define resources that the skin depends on.
I wanted to use that for bitmap fonts because I am creating them at runtime out of a .ttf file to be able to create the correct font sizes for the correct density/size of the target device's display.
Here is an example program that causes the issue:
public class Test extends Game {
private AssetManager assetManager;
private Skin skin;
private Batch batch;
@Override
public void create() {
this.assetManager = new AssetManager();
final FileHandleResolver resolver = new InternalFileHandleResolver();
assetManager.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver));
assetManager.setLoader(BitmapFont.class, ".ttf", new FreetypeFontLoader(resolver));
final ObjectMap<String, Object> resources = new ObjectMap<String, Object>();
final FreetypeFontLoader.FreeTypeFontLoaderParameter fontParam = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
fontParam.fontFileName = "ui/font.ttf";
fontParam.fontParameters.size = (int) (16 * Gdx.graphics.getDensity());
assetManager.load("font16.ttf", BitmapFont.class, fontParam);
assetManager.finishLoading();
resources.put("font_16", assetManager.get("font16.ttf", BitmapFont.class));
assetManager.load("ui/ui.json", Skin.class, new SkinLoader.SkinParameter(resources));
assetManager.finishLoading();
skin = assetManager.get("ui/ui.json", Skin.class);
batch = new SpriteBatch();
}
@Override
public void render() {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
skin.get("font_16", BitmapFont.class).draw(batch, "Test123", 10, 10);
batch.end();
}
@Override
public void dispose() {
super.dispose();
// enabling the next line will get rid of the Pixmap already disposed exception
// skin.remove("font_16", BitmapFont.class);
assetManager.dispose();
batch.dispose();
}
}
Now when disposing the assetManager at the end of the program then I got an exception "Pixmap already disposed" because the bitmap fonts got disposed multiple times (once when the Skin gets disposed and once when the Bitmap font itself gets disposed).
To solve that for now I called skin.remove(fontName, BitMapFont.class) before assetManager.dispose() to release the bitmap fonts only once BUT imo this is not a nice way and I would expect that the dependency handling of the assetManager should take care of that.
I checked the code of the SkinLoader class and to me it seems like those passed resources are not added as a dependency of the Skin and that is why this error occurs.
My question now is: Did I do something wrong or does anyone have a working code for that to show how the resourcesmap should be used in the correct way?
Found a topic which is related to this but seems like it never got a real answer
I have got the answer that I needed. Actually it is not an issue with the asset manager. It is an issue with the Skin class.
Look at the dispose method:
public void dispose () {
if (atlas != null) atlas.dispose();
for (ObjectMap<String, Object> entry : resources.values()) {
for (Object resource : entry.values())
if (resource instanceof Disposable) ((Disposable)resource).dispose();
}
}
It disposes any resource directly which is then not reflected in the asset manager of course and therefore the asset manager will also dispose the created bitmap fonts.
To solve this issue I wrote my own loader and Skin class. In case anyone is interested here is the code:
public class SkinLoader extends AsynchronousAssetLoader<Skin, SkinLoader.SkinParameter> {
public static class SkinParameter extends AssetLoaderParameters<Skin> {
private final String fontPath;
private final int[] fontSizesToCreate;
public SkinParameter(final String fontPath, final int... fontSizesToCreate) {
if (fontPath == null || fontPath.trim().isEmpty()) {
throw new GdxRuntimeException("fontPath cannot be null or empty");
}
if (fontSizesToCreate.length == 0) {
throw new GdxRuntimeException("fontSizesToCreate has to contain at least one value");
}
this.fontPath = fontPath;
this.fontSizesToCreate = fontSizesToCreate;
}
}
public SkinLoader(final FileHandleResolver resolver) {
super(resolver);
}
@Override
@SuppressWarnings("unchecked")
public Array<AssetDescriptor> getDependencies(final String fileName, final FileHandle file, final SkinParameter parameter) {
if (parameter == null) {
throw new GdxRuntimeException("SkinParameter cannot be null");
}
// texture atlas dependency
final Array<AssetDescriptor> dependencies = new Array<AssetDescriptor>();
dependencies.add(new AssetDescriptor(file.pathWithoutExtension() + ".atlas", TextureAtlas.class));
// bitmap font dependencies
for (int fontSize : parameter.fontSizesToCreate) {
final FreetypeFontLoader.FreeTypeFontLoaderParameter fontParam = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
fontParam.fontFileName = parameter.fontPath;
// enable anti-aliasing
fontParam.fontParameters.minFilter = Texture.TextureFilter.Linear;
fontParam.fontParameters.magFilter = Texture.TextureFilter.Linear;
// create font according to density of target device display
fontParam.fontParameters.size = (int) (fontSize * Gdx.graphics.getDensity());
dependencies.add(new AssetDescriptor("font" + fontSize + ".ttf", BitmapFont.class, fontParam));
}
return dependencies;
}
@Override
public Skin loadSync(final AssetManager manager, final String fileName, final FileHandle file, final SkinParameter parameter) {
// load atlas and create skin
final String textureAtlasPath = file.pathWithoutExtension() + ".atlas";
final TextureAtlas atlas = manager.get(textureAtlasPath, TextureAtlas.class);
final Skin skin = new Skin(atlas);
// add bitmap fonts to skin
for (int fontSize : parameter.fontSizesToCreate) {
skin.add("font_" + fontSize, manager.get("font" + fontSize + ".ttf"));
}
// load skin now because the fonts in the json file are now available
skin.load(file);
return skin;
}
@Override
public void loadAsync(final AssetManager manager, final String fileName, final FileHandle file, final SkinParameter parameter) {
}}
public class Skin extends com.badlogic.gdx.scenes.scene2d.ui.Skin {
Skin(final TextureAtlas atlas) {
super(atlas);
}
@Override
public void dispose() {
for (String bitmapFontKey : this.getAll(BitmapFont.class).keys()) {
remove(bitmapFontKey, BitmapFont.class);
}
super.dispose();
}}