Saturday, February 15, 2014

Libgdx - Generate Bitmap Fonts for Any Screen Size With Minimal Loading Time.

UPDATE: I got some requests, so I created a github repo to make it easy to include this functionality into your project.  This the best way to use this code.

https://github.com/jrenner/gdx-smart-font


This post will show you how to use the FreeTypeFontGenerator (a libgdx extension) and the BitmapFontWriter (included in gdx-tools) to dynamically generate your bitmap font to suit the screen size of the device the app is running on.  We will also optimize the process so that the fonts are only generated if there is no previously generated version found.

FreeTypeFontGenerator is not part of the core libgdx package (not in gdx.jar), to learn how to include it in your project, or use it in general, see this page on the wiki.

BitmapFontWriter is also not part of libgdx core.  It is located in the gdx-tools.jar.  Fortunately, it has little dependencies, which means you can just copy/paste the code of the BitmapFontWriter class directly into your project as a single file, and get it working with little alterations.

Once you get those two things setup, you are ready to go.  Below is the code you need to accomplish font generation/loading.

We get three main benefits from this code:
#1 No need to pre-render font bitmaps using Hiero or some other tool

#2 Fonts are perfectly (more-or-less) sized in proportion to whatever screen size the app is launched with.  Screen-size changes in between launch are also handled. (For mid-game resizing, you would have to do some more work)

#3 By saving the generated fonts to file, we can load from file in subsequent startups where the screen size has not changed (On Android, it should never change).  In my case, this cuts down the loading time for fonts dramatically, make the app start up much snappier.

In my case, where I generate 4 fonts, generating fonts took 3765ms, while loading the pre-generated fonts only took 325ms.  That's more than 3 seconds shaved off of app startup time!

In the images below, notice how despite the different window sizes, the fonts retain the same proportion.  The same effect will occur on Android screens.


1280x720

800x480



First, let's take a look at the code generating the fonts:

private static void generateFonts() {
// if fonts are already generated, just load from file
Preferences fontPrefs = Gdx.app.getPreferences("org.jrenner.superior.font");
int displayWidth = fontPrefs.getInteger("display-width", 0);
int displayHeight = fontPrefs.getInteger("display-height", 0);
boolean loaded = false;
if (displayWidth != Gdx.graphics.getWidth() || displayHeight != Gdx.graphics.getHeight()) {
Tools.log.debug("Screen size change detected, regenerating fonts");
} else {
try {
// try to load from file
Tools.log.debug("Loading generated fonts from file cache");
smallGeneratedFont = new BitmapFont(getFontFile("exo-small.fnt"));
normalGeneratedFont = new BitmapFont(getFontFile("exo-normal.fnt"));
largeGeneratedFont = new BitmapFont(getFontFile("exo-large.fnt"));
monoGeneratedFont = new BitmapFont(getFontFile("lib-mono.fnt"));
loaded = true;
} catch (GdxRuntimeException e) {
Tools.log.debug("Couldn't load pre-generated fonts. Will generate fonts.");
}
}
if (!loaded || forceGeneration) {
forceGeneration = false;
float width = Gdx.graphics.getWidth();
float ratio = width / 1280f; // use 1920x1280 as baseline, arbitrary
float baseSize = 28f; // for 28 sized fonts at baseline width above
// define other sizes
int size = (int) (baseSize * ratio);
int smallSize = (int) (size * 0.75f);
int largeSize = (int) (size * 1.5f);
// store screen width for detecting screen size change
// on later startups, which will require font regeneration
fontPrefs.putInteger("display-width", Gdx.graphics.getWidth());
fontPrefs.putInteger("display-height", Gdx.graphics.getHeight());
fontPrefs.flush();
FileHandle exoFile = Gdx.files.internal("fonts/Exo-Regular.otf");
int pageSize = 512; // size of atlas pages for font pngs
smallGeneratedFont = generateFontWriteFiles("exo-small", exoFile, smallSize, pageSize, pageSize);
normalGeneratedFont = generateFontWriteFiles("exo-normal", exoFile, size, pageSize, pageSize);
largeGeneratedFont = generateFontWriteFiles("exo-large", exoFile, largeSize, pageSize, pageSize);
FileHandle libMonoFile = Gdx.files.internal("fonts/LiberationMono-Regular.ttf");
monoGeneratedFont = generateFontWriteFiles("lib-mono", libMonoFile, smallSize, pageSize, pageSize);
}
}
view raw generate.java hosted with ❤ by GitHub

Now, let's see how we save the generated fonts to file.  These methods could be directly copied and pasted into your project without alteration.
/** Convenience method for generating a font, and then writing the fnt and png files.
* Writing a generated font to files allows the possibility of only generating the fonts when they are missing, otherwise
* loading from a previously generated file.
* @param fontFile
* @param fontSize
*/
private static BitmapFont generateFontWriteFiles(String fontName, FileHandle fontFile, int fontSize, int pageWidth, int pageHeight) {
FreeTypeFontGenerator generator = new FreeTypeFontGenerator(fontFile);
PixmapPacker packer = new PixmapPacker(pageWidth, pageHeight, Format.RGBA8888, 2, false);
FreeTypeBitmapFontData fontData = generator.generateData(fontSize, FreeTypeFontGenerator.DEFAULT_CHARS, false, packer);
Array<Page> pages = packer.getPages();
TextureRegion[] texRegions = new TextureRegion[pages.size];
for (int i=0; i<pages.size; i++) {
Page p = pages.get(i);
Texture tex = new Texture(new PixmapTextureData(p.getPixmap(), p.getPixmap().getFormat(), false, false, true)) {
@Override
public void dispose () {
super.dispose();
getTextureData().consumePixmap().dispose();
}
};
tex.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
texRegions[i] = new TextureRegion(tex);
}
BitmapFont font = new BitmapFont(fontData, texRegions, false);
saveFontToFile(font, fontSize, fontName, packer);
generator.dispose();
packer.dispose();
return font;
}
private static boolean saveFontToFile(BitmapFont font, int fontSize, String fontName, PixmapPacker packer) {
FileHandle fontFile = getFontFile(fontName + ".fnt"); // .fnt path
FileHandle pixmapDir = getFontFile(fontName); // png dir path
BitmapFontWriter.setOutputFormat(OutputFormat.Text);
String[] pageRefs = BitmapFontWriter.writePixmaps(packer.getPages(), pixmapDir, fontName);
Tools.log.debug(String.format("Saving font [%s]: fontfile: %s, pixmapDir: %s\n", fontName, fontFile, pixmapDir));
// here we must add the png dir to the page refs
for (int i = 0; i < pageRefs.length; i++) {
pageRefs[i] = fontName + "/" + pageRefs[i];
//Tools.log.debug("\tpageRef: " + pageRefs[i]);
}
BitmapFontWriter.writeFont(font.getData(), pageRefs, fontFile, new FontInfo(fontName, fontSize), 1, 1);
return true;
}
private static FileHandle getFontFile(String filename) {
return Gdx.files.local("generated-fonts/" + filename);
}