Saturday, July 2, 2016

Phaser tutorial: Fun with bitmap fonts

 





Previous Phaser tutorials and articles:
Phaser tutorial: Using Spriter player for Phaser
Phaser tutorial: Merging fonts into sprite atlas
Phaser: Typescript defs for Phaser Box2D plugin
Phaser tutorial: Spriter Pro features added to Spriter player for Phaser
Phaser tutorial: Using Phaser signals
Phaser tutorial: Breaking the (z-order) law!
Phaser tutorial: Phaser and Spriter skeletal animation
Phaser tutorial: DronShooter - simple game in Typescript - Part 3
Phaser tutorial: DronShooter - simple game in Typescript - Part 2
Phaser tutorial: adding 9-patch image support to Phaser
Phaser tutorial: DronShooter - simple game in Typescript - Part 1
Phaser tutorial: custom easing functions for tweening and easing functions with parameters
Phaser tutorial: sprites and custom properties for atlas frames
Phaser tutorial: manage different screen sizes
Phaser tutorial: How to wrap bitmap text


Introduction


 Very often I need to display some information for player in GUI in my games. It is usually composed of some text and icon. And it also often takes some time to fine tune positioning of text and icon. Things get complicated if part of it can change width like number on following image:


 Then, if you update number, you also have to update position of icon. Making some longer text, like the one on image bellow, with small images inside of it, is tedious work and usually needs lot of code.



Solution


 Fortunately, solution is easy. We can add images as special characters into font and then print whole text, including images, in one call in code.
 Let's say you have font called "Font" and atlas called "Game". Font is made of texture with characters and xml metadata file. It can be font made with Littera or any other font tool. Atlas is standard atlas with your game images.
 After these are loaded in your preload function - for example like this:

            this.load.atlas("Game", "assets/Game.png", "assets/Game.json");
            this.load.bitmapFont("Font", "assets/Font.png", "assets/Font.xml");

 you can start your adjustments in create function. First we get reference to our loaded assets:

            let atlas = this.cache.getImage("Game", true);
            let font = this.cache.getBitmapFont("Font");

 Next we get reference to loaded font xml data and also reference to capital "A" character. Reason for this is, that we will center added images on the same level as center of "A" is.

            let fontData = font.font;
            let charA = fontData.chars[65];

 Now, in my case, I have 7 gem images in atlas with names Gem1 ... Gem7. I will add them as special characters. First, I have to choose character code for them. Here I chose 5000 for Gem1, 5001 for Gem2 ...

            for (let i = 0; i < 7; i++) {
                let f = this.cache.getFrameByName("Game", "Gem" + (i + 1));

                fontData.chars[5000 + i] = {
                    x: f.x,
                    y: f.y,
                    width: f.width,
                    height: f.height,
                    xOffset: 1,
                    yOffset: charA.yOffset + Math.floor((charA.height - f.height) / 2),
                    xAdvance: f.width + 2,
                    kerning: [],
                    texture: new PIXI.Texture(atlas["base"], new PIXI.Rectangle(f.x, f.y, f.width, f.height)) 
                };
            }

 First, we get Phaser.Frame from atlas, so we will have access to position and dimensions of image within atlas. Next, we add new object with all font metadata to current font characters data at position of character code we chose before. For yOffset we are doing small calculation to center added images relative to "A" - we want center of A and added images on the same level. xOffset is 1 and it says, that there will be 1 pixel space after previous character. xAdvance then says how much has font renderer step to draw next character. We set this to width + 2 (= xOffset + width + 1). On last line, we are setting texture property. Here you may get error - just head to phaser.d.ts and change BMFontChar interface to use PIXI.Texture instead of PIXI.BaseTexture.


Test


 To test our gem images in text add this code:

            let text = "Hi, " + String.fromCharCode(5000, 5005, 5006) + " there!\n\nHow are you? " + String.fromCharCode(5001, 5002);
            let bmText = new Phaser.BitmapText(this.game, 0, 0, "Font", text, 110);
            bmText.anchor.x = 0.5;
            this.world.add(bmText);

 To include gems we make string from character codes with call to String.fromCharCode(). You should see this on screen:



Draw calls


 While everything works, there is small issue you should be aware of. As our font images and gem images are in different textures, there can be more draw calls. In above case there are four draw calls to draw this single piece of text.

 Fortunately, there is also solution for this. Merge your font into atlas as described in one of previous tutorials. Code changes only very little:


            let font = this.cache.getBitmapFont("Font");
            let fontData = font.font;
            let charA = fontData.chars[65];

            for (let i = 0; i < Level.MAX_GEM; i++) {
                let f = this.cache.getFrameByName("Game", "Gem" + (i + 1));

                fontData.chars[5000 + i] = {
                    x: f.x,
                    y: f.y,
                    width: f.width,
                    height: f.height,
                    xOffset: 1,
                    yOffset: charA.yOffset + Math.floor((charA.height - f.height) / 2),
                    xAdvance: f.width + 2,
                    kerning: [],
                    texture: new PIXI.Texture(font.base, new PIXI.Rectangle(f.x, f.y, f.width, f.height)) 
                };
            }

 You do not need reference to atlas, because gem images are in the same texture as font characters and in last line you can get base texture from font data.

 This reduces draw calls to one.


Conclusion


 With presented solution you can easily mix text with icons. If you merge font with atlas, there are no additional costs in terms of draw calls.