1. Words in games

In the first of our series of articles, we explained how to render the text “Hello World” on stage in the game.

However, the old brother who often plays the game must know that there are not only ordinary words in the game, but also all kinds of fancy fonts. How do we realize these words?

As you can see in the screenshot below of the game’s gear interface, there are a lot of fancy fonts that you can’t use to render normal fonts, or rely on font libraries because they have italics, shadows, strokes, gradients, etc.

In order to solve the above people for the pursuit of cool text, in the H5 game there is a more called bitmap font, specially used to render the cool text above.

What is a bitmap font? As the name implies, a Bitmap Font, or Bitmap Font, is a Font scheme that renders prefabricated characters as images on a screen. Because it is a picture, he is good at strokes, shadows and gradients. A bitmap font file itself consists of two parts:

  • Character atlas (*.png)
  • A configuration file that describes the size and position of characters in the atlas (*.fnt)

By rendering the text one-to-one as an image, what a cool static effect is easy for us

How can I use words in games?

So, to briefly summarize, there are currently two types of fonts used for text rendering in games:

  1. Regular font
  2. Bitmap fonts

Now that we know what fonts are available in the game, we can take a look at how to code the text we want into the interface.

2.1 Common Fonts

In common fonts, we can also divide fonts into local font library and asynchronously loaded TTF font library according to whether the font library is built into the system. So let’s see separately how do we do that in code

2.1.1 Local Font Library

First, we need to create a new Laya project, and then add the following code to the start scenario

/** * uses local plain text */
public addText() {
    let $text = new Laya.Text();

    $text.font = '宋体';
    $text.fontSize = 30;
    $text.color = '#fff';
    $text.text = 'Qwertyuiop one two three four five six seven eight';
    $text.pos(0.400);

    this.addChild($text);
}
Copy the code

The effect is as follows:

2.1.2 Loading TTF font library asynchronously

If your text needs to be loaded from the CDN, you need to add the following steps to the code above:

  1. Load the fonts
  2. Get font name
  3. Set the font

The code is as follows:

* @param font * @param localPath */
public static async loadFont(onlinePath, localPath): Promise<string> {
    // First try to load the font locally
    let fontFamily = window['qg'].loadFont(localPath);
    try {
        if(! fontFamily || fontFamily ==='null') {
            // Load the font asynchronously and use it again next time
             window['qg'].downloadFile({
                url: onlinePath,
                filePath: localPath
            });
        }
        return fontFamily;
    } catch (e) {
        // Return the default font
        return 'Microsoft YaHei'; }}Copy the code

The remaining steps are no different from using the local font library

$text.font = loadFont('xxx'.'xxx');
Copy the code

2.2 Bitmap fonts

Compared with ordinary fonts, bitmap fonts are difficult to make bitmap font library. Here I directly to 2 portal, you can directly see how to make bitmap font, Mac how to make bitmap font, Windows how to make bitmap font.

After making bitmap fonts, you will get a set of fonts like this.

How bitmap fonts are parsed and rendered will be covered in the next section.

Here we begin by looking at how to use bitmap fonts in Laya. There are two ways to use bitmap fonts in Laya:

  1. throughLabelorTextSet font directly to bitmap font;
  2. throughFontClipWith simple bitmap pictures, render bitmap;

2.2.1 Label or Text components

/** ** Use bitmap font */
public addBitmap() {
    this.loadBitmapFont('purple'.64).then((a)= > {
        let $text = new Laya.Text();

        $text.font = 'purple';
        $text.fontSize = 64;
        $text.text = '1234567890';
        $text.pos(0.600);

        this.addChild($text);
    });
}

/** * Load bitmap font */
public loadBitmapFont(fontName: string, fontSize: number) {
    return new Promise((resolve) = > {
        let bitmapFont = new Laya.BitmapFont();
        // Font size
        bitmapFont.fontSize = fontSize;
        // Allow setting automatic scaling
        bitmapFont.autoScaleSize = true;
        // Load the font
        bitmapFont.loadFont(
            `bitmapFont/${fontName}.fnt`.new Laya.Handler(this, () = > {// Set the width of the space
                bitmapFont.setSpaceWidth(10);
                // Register bitmap fonts according to the font nameLaya.Text.registerBitmapFont(fontName, bitmapFont); resolve(fontName); })); }); }Copy the code

The effect is as follows:

2.2.2 FontClip components

/** * use text to slice */
public addFontClip() {
    let $fontClip = new Laya.FontClip();

    $fontClip.skin = 'bitmapFont/fontClip.png';
    $fontClip.sheet = '0123456789';
    $fontClip.value = '012345';
    $fontClip.pos(0.800);

    this.addChild($fontClip);
}
Copy the code

The effect is as follows:

2.2.3 Application Scenario Summary

type scenario
Label, Text(plain font) Regular font
Label, Text(bitmap font) Lots of fancy, complex fonts
FontClip(Simple bitmap) Small, simple, cool fonts

3. How is the text rendered?

Now that we know how to write the code, let’s see, how does the engine actually render?

3.1 Unique Concepts

The following concepts will appear in the following articles, which are introduced here in advance:

  • Sprite: This is the display list node for Laya’s basic display graphics and the only core display class in Laya.
  • Graphic: A drawing object that encapsulates the interface for drawing bitmaps and vector Graphics. All Sprite drawing operations are implemented via Graphics.
  • Texture: In the OpenGL era, Texture was a noun from the perspective of app developers that physically referred to a contiguity of GPU memory, and this concept is carried over into H5 games.

In the Laya engine, the components we use have the following inheritance:

All components are fully integrated with Sprite objects and rendered through Graphic objects in Sprite objects.

3.2 How to render FontClip

FontClip is a simplified version of a bitmap font. You can use it by setting up a slice image and text content.

The entire rendering process of FontClip can be summarized as follows:

1. Load the font and save the character’s Texture according to the specified slice size

/** * @private * Load slice image resource completion function. * @param Url resource address. * @param img texture. * /
protected loadComplete(url: string, img: Texture): void {
    if (url === this._skin && img) {
        var w: number = this._clipWidth || Math.ceil(img.sourceWidth / this._clipX);
        var h: number = this._clipHeight || Math.ceil(img.sourceHeight / this._clipY);

        var key: string = this._skin + w + h;
        / /... Omit non-critical code
        for (var i: number = 0; i < this._clipY; i++) {
            for (var j: number = 0; j < this._clipX; j++) {
                this._sources.push(Texture.createFromTexture(img, w * j, h * i, w, h));
            }
        }
        WeakObject.I.set(key, this._sources);

        this.index = this._index;
        this.event(Event.LOADED);
        this.onCompResize(); }}Copy the code

2. Parse the sheet field to indicate the position of each character in the bitmap based on user input. Sets the bitmap font content, with Spaces representing line breaks. For example, “abc123 456” indicates that the first line corresponds to “abc123”, and the second line corresponds to “456”.

set sheet(value: string) {
    value += ' ';
    this._sheet = value;
    // Wrap lines according to Spaces
    var arr: any[] = value.split(' ');
    this._clipX = String(arr[0]).length;
    this.clipY = arr.length;

    this._indexMap = {};
    for (var i: number = 0; i < this._clipY; i++) {
        var line: any[] = arr[i].split(' ');
        for (var j: number = 0.n: number = line.length; j < n; j++) {
            this._indexMap[line[j]] = i * this._clipX + j; }}}Copy the code

3. Find the corresponding texture according to the character, and render the corresponding character using the graphic in the component

protected changeValue(): void {
    / /... Omit non-critical code

    // re-render
    for (var i: number = 0.sz: number = this._valueArr.length; i < sz; i++) {
        var index: number = this._indexMap[this._valueArr.charAt(i)];
        if (!this.sources[index]) continue;
        texture = this.sources[index];
        
        / /... Omit non-critical code
        this.graphics.drawImage(
            texture,
            0 + dX,
            i * (texture.sourceHeight + this.spaceY),
            texture.sourceWidth,
            texture.sourceHeight
        );
    }

    / /... Omit non-critical code
}
Copy the code

3.3 How to parse BitmapFont

The above is a simple BitmapFont rendering, and our BitmapFont rendering is all dependent on UI components, his process includes BitmapFont parsing, BitmapFont rendering 2 blocks. Here’s how to parse a normal Bitmap.

The difference between BitmapFont and FontCLip is that BitmapFont saves the rules for characters and images to an XML file. We just need to parse the XML file normally and get the rules. The overall process is the same as FontClip.

PNG and bitmap information. FNT are two parts of a bitmap font.

<?xml version="1.0" encoding="UTF-8"? >
<! --Created using Glyph Designer - http://71squared.com/glyphdesigner-->
<font>
    <info face="ColorFont" size="64" bold="1" italic="0" charset="" unicode="0" stretchH="100" smooth="1" aa="1" padding="0,0,0,0" spacing="2"/>
    <common lineHeight="64" base="88" scaleW="142" scaleH="200" pages="1" packed="0"/>
    <pages>
        <page id="0" file="purple.png"/>
    </pages>
    <chars count="10">
        <char id="48" x="108" y="2" width="32" height="48" xoffset="3" yoffset="10" xadvance="37" page="0" chnl="0" letter="0"/>
        <char id="49" x="38" y="102" width="21" height="47" xoffset="5" yoffset="10" xadvance="37" page="0" chnl="0" letter="1"/>
        <char id="50" x="2" y="2" width="34" height="48" xoffset="2" yoffset="9" xadvance="37" page="0" chnl="0" letter="2"/>
        <char id="51" x="38" y="2" width="33" height="48" xoffset="2" yoffset="10" xadvance="37" page="0" chnl="0" letter="3"/>
        <char id="52" x="104" y="52" width="35" height="47" xoffset="2" yoffset="10" xadvance="37" page="0" chnl="0" letter="4"/>
        <char id="53" x="2" y="52" width="32" height="48" xoffset="3" yoffset="10" xadvance="37" page="0" chnl="0" letter="5"/>
        <char id="54" x="73" y="2" width="33" height="48" xoffset="3" yoffset="10" xadvance="37" page="0" chnl="0" letter="6"/>
        <char id="55" x="2" y="102" width="34" height="47" xoffset="2" yoffset="10" xadvance="37" page="0" chnl="0" letter="Seven"/>
        <char id="56" x="36" y="52" width="32" height="48" xoffset="3" yoffset="10" xadvance="37" page="0" chnl="0" letter="8"/>
        <char id="57" x="70" y="52" width="32" height="48" xoffset="3" yoffset="9" xadvance="37" page="0" chnl="0" letter="9"/>
    </chars>
    <kernings count="0"/>
</font>
Copy the code

The key nodes in the above information are INFO, common and chars, which record the font type, line height and corresponding character position of the current bitmap font in detail.

Let’s look at Laya source code is how to do bitmap font parsing:

1. Load fonts

/** * Load the bitmap font file by specifying the bitmap font file path. After loading the bitmap font file, it will be automatically parsed. * @param path Bitmap font file path. * @param complete loads and parses the completed callback. * /
loadFont(path: string, complete: Handler): void {
    this._path = path;
    this._complete = complete;

    / /... Omit non-critical code
    
    // Load the XML and the corresponding font image
    ILaya.loader.load(
        [
            { url: path, type: ILaya.Loader.XML },
            { url: path.replace('.fnt'.'.png'), type: ILaya.Loader.IMAGE },
        ],
        Handler.create(this.this._onLoaded)
    );
}
Copy the code

Parse the XML && according to the information parse the font, generate the character and Texture mapping

/** * parse font files. * @param XML font file XML. * @param Texture font texture. * /
parseFont(xml: XMLDocument, texture: Texture): void {
    / /... Omit non-critical code
    
    // Parse the XML file to get the corresponding parameters
    var tX: number = 0;
    var tScale: number = 1;

    var tInfo: any = xml.getElementsByTagName('info');
    if(! tInfo[0].getAttributeNode) {
        return this.parseFont2(xml, texture);
    }
    this.fontSize = parseInt(tInfo[0].getAttributeNode('size').nodeValue);

    var tPadding: string = tInfo[0].getAttributeNode('padding').nodeValue;
    var tPaddingArray: any[] = tPadding.split(', ');
    this._padding = [
        parseInt(tPaddingArray[0]),
        parseInt(tPaddingArray[1]),
        parseInt(tPaddingArray[2]),
        parseInt(tPaddingArray[3]]),// Read the position of each image according to the chars field
    var chars = xml.getElementsByTagName('char');
    var i: number = 0;
    for (i = 0; i < chars.length; i++) {
        var tAttribute: any = chars[i];
        var tId: number = parseInt(tAttribute.getAttributeNode('id').nodeValue);

        var xOffset: number = parseInt(tAttribute.getAttributeNode('xoffset').nodeValue) / tScale;
        var yOffset: number = parseInt(tAttribute.getAttributeNode('yoffset').nodeValue) / tScale;
        var xAdvance: number = parseInt(tAttribute.getAttributeNode('xadvance').nodeValue) / tScale;

        var region: Rectangle = new Rectangle();
        region.x = parseInt(tAttribute.getAttributeNode('x').nodeValue);
        region.y = parseInt(tAttribute.getAttributeNode('y').nodeValue);
        region.width = parseInt(tAttribute.getAttributeNode('width').nodeValue);
        region.height = parseInt(tAttribute.getAttributeNode('height').nodeValue);

        var tTexture: Texture = Texture.create(
            texture,
            region.x,
            region.y,
            region.width,
            region.height,
            xOffset,
            yOffset
        );
        this._maxWidth = Math.max(this._maxWidth, xAdvance + this.letterSpacing);
        // font dictionary
        this._fontCharDic[tId] = tTexture;
        this._fontWidthMap[tId] = xAdvance; }}Copy the code

3.4 Label Process of rendering text

The Label rendering itself uses the Text in its component to achieve the final rendering. Below is the flow chart of Text rendering:

There are three core text rendering methods: typeset, changeText, and _renderText. If we look at the source code, the code saves only the key steps.

1. Typeset

/** * 

Typeset text.

*

Perform width and height calculations, render and redraw text.

*/
typeset(): void { / /... Omit non-critical code // No text, direct clear sky if (!this._text) { this._clipPoint = null; this._textWidth = this._textHeight = 0; this.graphics.clear(true); return; } / /... Omit non-critical code // recalculate the row height this._lines.length = 0; this._lineWidths.length = 0; if (this._isPassWordMode()) { // If it is password, the status should be calculated using the password symbol this._parseLines(this._getPassWordTxt(this._text)); } else this._parseLines(this._text); / /... Omit non-critical code // More padding to calculate lineHeight this._evalTextSize(); // Render the font this._renderText(); } Copy the code

ChangeText just changes the text

/** * 

Quickly change the display text. No typesetting calculation, high efficiency.

*

If you change only the text content but not the text style, you are advised to use this interface to improve efficiency.

* @param text Specifies the text content. * /
changeText(text: string): void { if (this._text ! == text) {// Set the language pack this.lang(text + ' '); if (this._graphics && this._graphics.replaceText(this._text)) { // Replace the text successfully and do nothing //repaint(); } else { / / layout this.typeset(); }}}Copy the code

_renderText renders text

/** * @private * Render text. * @param begin The index of the rows to start rendering. * @param visibleLineCount Number of rendered lines. * /
protected _renderText(): void {
    var padding: any[] = this.padding;
    var visibleLineCount: number = this._lines.length;

    // If overflow is scroll or visible, truncate rows
    if (this.overflow ! = Text.VISIBLE) { visibleLineCount =Math.min(
            visibleLineCount,
            Math.floor((this.height - padding[0] - padding[2)/(this.leading + this._charSize.height)) + 1
        );
    }

    // Clear the canvas
    var graphics: Graphics = this.graphics;
    graphics.clear(true);

    // Handle vertical alignment
    var startX: number = padding[3];
    var textAlgin: string = 'left';
    var lines: any[] = this._lines;
    var lineHeight: number = this.leading + this._charSize.height;
    vartCurrBitmapFont: BitmapFont = (<TextStyle>this._style).currBitmapFont; if (tCurrBitmapFont) { lineHeight = this.leading + tCurrBitmapFont.getMaxHeight(); } var startY: number = padding[0]; // handle horizontal alignment if (! tCurrBitmapFont && this._width > 0 && this._textWidth <= this._width) { if (this.align == 'right') { textAlgin = 'right'; startX = this._width - padding[1]; } else if (this.align == 'center') { textAlgin = 'center'; StartX = this._width * 0.5 + padding[3] -padding [1]; } } if (this._height > 0) { var tempVAlign: string = this._textHeight > this._height ? 'top' : this.valign; If (tempVAlign === 'middle') startY = (this._height - visibleLineCount * lineHeight) * 0.5 + padding[0] -padding [2]; else if (tempVAlign === 'bottom') startY = this._height - visibleLineCount * lineHeight - padding[2]; } / /... Var x: number = 0, y: number = 0; var end: number = Math.min(this._lines.length, visibleLineCount + beginLine) || 1; for (var i: number = beginLine; i < end; i++) { // ... If (tCurrBitmapFont) {// Var tWidth: number = this.width; tCurrBitmapFont._drawText(word, this, x, y, this.align, tWidth); } else { // ... Omit non-critical code _word.settext (word); (<WordText>_word).splitRender = graphics.fillText(_word, x, y, ctxFont, this.color, textAlgin); }} / / bitmap fonts automatic scaling the if (tCurrBitmapFont && tCurrBitmapFont. AutoScaleSize) {var tScale: number = 1 / bitmapScale; this.scale(tScale, tScale); } if (this._clipPoint) graphics.restore(); this._startX = startX; this._startY = startY; }Copy the code

Four, LayA2. x game engine introduction series

The author has been deeply involved in an OPPO fast game project (similar to micro games on wechat) since May, 19. From scratch, I have finally entered the door of H5 game development. There aren’t many tutorials on how to make fast games with Laya, so I decided to write down all the holes I’ve stepped on, solved, and learned over the past few months so that other students can avoid them in advance.

The LayA2.x Game Engine Primer series is expected to write the following articles documenting how to develop and ship a fast game from scratch:

  • Laya2.x Game Engine Introduction Series 1: Hello World
  • Laya2.x Game Engine Introduction Series (ii) : UI interface development
  • Laya2.x Game Engine Introduction series (iii) : Common animation development
  • Laya2.x Game engine introduction series (4) : Pixel level restore text
  • Laya2.x Game Engine Introduction Series (5) : The Soul of the game – Script
  • Laya2.x Game Engine Introduction Series (6) : Pictures cure all Diseases
  • Laya2.x Game Engine Introduction Series (7) : Data Communication
  • Laya2.x Game Engine Introduction Series (8) : 2D physics World
  • Laya2.x Game Engine Introduction Series (9) : Game debugging
  • Laya2.x Game Engine Introduction Series (10) : Project Engineering
  • Laya2.x Game Engine Introduction Series (11) : Game performance Optimization
  • Laya2.x Game Engine Introduction Series (12) : FAQ

At the same time, Laya2 currently engine code through TypeScript refactoring, if you have any questions in writing code can directly find the answer in GitHub source code, the author will also write some articles about Laya2 source code parsing, interested friends can pay attention to.

This is the first time to try to write a complete teaching article, if there is any mistake or not rigorous place, please be sure to give correction, thank you very much!

About me

I am a modefeelings code porter, will update 1 to 2 front-end related articles every week, interested in the old iron can scan the following TWO-DIMENSIONAL code attention or direct wechat search front-end cram school attention.

Mastering the front end is difficult, let’s make up the lesson together!