Laya2.x game engine introduction series: pixel level restore text

Keywords: xml REST Mac Windows

Text in the game

The first article in our series tells you how to render the text "Hello World" on the stage in the game.

However, the elder 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 can we realize these words?

We can see that in the screenshot of the game equipment interface below, there are a lot of fancy fonts, which can't be realized by rendering ordinary fonts, or it will be very complex to realize by font library, because they have italics, shadows, strokes, gradients, etc.

In order to solve the above everyone's pursuit of cool text, there is a new kind of thing called bitmap font in H5 game, which is specially used to render cool text above.

What is a Bitmap Font? As the name implies, Bitmap Font, that is, Bitmap Font, is a font scheme that renders prefabricated characters in the form of pictures on the screen. Because it is a picture, he is proficient in edge drawing, shadow and gradient. A set of Bitmap Font file itself consists of two parts:

  • Character Atlas (*. png)
  • A configuration file that describes the size and location of characters in the graph set (*. fnt)

Through one-to-one rendering of text as a picture, what a cool static effect is very simple for us ~

How can I use words in the game?

Well, let's summarize briefly. At present, there are two types of fonts used for text rendering in the game:

  1. Normal font
  2. bitmap font

Knowing what fonts are in the game, we can see how to insert the text we want into the interface in the form of code.

Normal font

In common fonts, we can divide font usage into two categories according to whether the font library is built-in in the system: local font library and asynchronous loading TTF font library. Let's take a look at how to implement it in code

a. Local font library

First, we need to create a new laya project, and then add the following code to the starting scene

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

    $text.font = 'Song style';
    $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 code

The effect is as follows:

b. Load TTF font library asynchronously

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

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

The code is as follows:

/**
 * Load font, simplified version omits many checking logic
 * @param font
 * @param localPath
 */
public static async loadFont(onlinePath, localPath): Promise<string> {
    // Try loading fonts locally first
    let fontFamily = window['qg'].loadFont(localPath);
    try {
        if (!fontFamily || fontFamily === 'null') {
            // Load fonts asynchronously and reuse them next time
             window['qg'].downloadFile({
                url: onlinePath,
                filePath: localPath
            });
        }
        return fontFamily;
    } catch (e) {
        // Return to default font
        return 'Microsoft YaHei';
    }
}
Copy code

The rest of the procedure is the same as using the local font library

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

bitmap font

Compared with ordinary fonts, the trouble of bitmap fonts is how to make bitmap font library. Here I'll give you two portal directly. You can see how to make bitmap font directly, How to make bitmap font for Mac,How to make bitmap font in Windows.

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

How to parse and render bitmap fonts will be discussed in the next section.

Let's start with how to use bitmap fonts in LAYA. There are two ways to use bitmap fonts in Laya:

  1. Set the font to bitmap font directly through Label or Text;
  2. Through FontClip with simple bitmap image, rendering bitmap;

a. Label or Text component

/**
 * Use bitmap font
 */
public addBitmap() {
    this.loadBitmapFont('purple', 64).then(() => {
        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 autoscale
        bitmapFont.autoScaleSize = true;
        // Load font
        bitmapFont.loadFont(
            `bitmapFont/${fontName}.fnt`,
            new Laya.Handler(this, () => {
                // Set the width of the space
                bitmapFont.setSpaceWidth(10);
                // Register bitmap font based on font name
                Laya.Text.registerBitmapFont(fontName, bitmapFont);
                resolve(fontName);
            })
        );
    });
}
Copy code

The effect is as follows:

b. FontClip components

/**
 * Using text slices
 */
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 code

The effect is as follows:

c. Use scenario summary

type scene
Label, text (normal font) General font
Label, text (bitmap font) Lots of complicated Cool Fonts
Fontclip (simple bitmap) A few, simple Cool Fonts

How is the text rendered?

We know how the code is written. Let's see how the engine renders?

Unique concept

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

  • Sprite: it is the display list node of the basic display graphics in Laya and the only core display class in LAYA.
  • Graphic: drawing object, encapsulating the interface of drawing bitmap and vector graph. All drawing operations of Sprite are realized by Graphics.
  • Texture: in the era of OpenGL, from the perspective of application developers, texture is a noun. Physically, it refers to a continuous space in GPU memory. This concept also extends to H5 games.

In the Laya engine, the inheritance relationship of the components we use is as follows:

All components are integrated with Sprite object, and the rendering is done through Graphic object in sprite object.

How FontClip renders

FontClip font slice, a simplified version of bitmap font, can be used only by setting a slice picture and text content, with the same effect as bitmap font.

The whole rendering process of FontClip is simply as follows:

1. Load the font and save the Texture corresponding to the character 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 code

2. Parse the sheet field to indicate the corresponding position of each character in the bitmap according to the user input. Set the content of bitmap font. The space represents line feed. For example, "abc123 456" means "abc123" in the first line and "456" in the second line.

set sheet(value: string) {
    value += '';
    this._sheet = value;
    //Wrap by space
    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 code

3. Find the corresponding texture according to the characters, and use the graphic in the component to render the corresponding characters

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 code

How to parse BitmapFont

The above is the rendering of simple Bitmap font, and the rendering of our Bitmap font all depends on the UI components. His process includes BitmapFont parsing and BitmapFont rendering. Let's talk about how to parse a normal Bitmap first.

In fact, the parsing process is also very simple. The difference between BitmapFont and fontclip is that BitmapFont saves the rules of characters and pictures to an XML file. We just need to parse the XML file normally and get the rules. The overall process is consistent with fontclip.

We mentioned earlier that a set of bitmap font includes two parts: bitmap *. png and bitmap information *. fnt. Let's see exactly what this bitmap information *. fnt contains.

<?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,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="7"/>
        <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 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 see how to parse bitmap font in Laya source code:

1. Load font

/**
 * Load the bitmap font file by specifying the path of the bitmap font file, and it will be automatically resolved after loading.
 * @param	path		The path to the bitmap font file.
 * @param	complete	Load and parse the completed callback.
 */
loadFont(path: string, complete: Handler): void {
    this._path = path;
    this._complete = complete;

    // ... omit non critical code
    
    // Load xml and 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 code

2. Parsing XML & & parsing fonts based on information to generate the mapping between characters and Texture

/**
 * Resolve font files.
 * @param	xml			Font file XML.
 * @param	texture		The texture of the font.
 */
parseFont(xml: XMLDocument, texture: Texture): void {
    // ... omit non critical code
    
    // Parsing xml file to get 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 corresponding position of each picture 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 code

The process of Label rendering text

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

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

1. typeset typesetting

/**
 * <p>Typesetting text. </p>
 * <p>Calculate width and height, render and redraw text. </p>
 */
typeset(): void {
    // ... omit non critical code

    // No words, clear sky
    if (!this._text) {
        this._clipPoint = null;
        this._textWidth = this._textHeight = 0;
        this.graphics.clear(true);
        return;
    }
    
    // ... omit non critical code

    // Recalculate row height
    this._lines.length = 0;
    this._lineWidths.length = 0;
    if (this._isPassWordMode()) {
        //If it is the password display status, the password symbol should be used for calculation
        this._parseLines(this._getPassWordTxt(this._text));
    } else this._parseLines(this._text);
    
    // ... omit non critical code

    // More padding calculation lineHeight
    this._evalTextSize();

    // Render fonts
    this._renderText();
}
Copy code

2. changeText just changes the text

/**
 * <p>Quickly change the display text. No typesetting calculation, high efficiency. </p>
 * <p>If you only change the text content and do not change the text style, this interface is recommended to improve efficiency. </p>
 * @param text Text content.
 */
changeText(text: string): void {
    if (this._text !== text) {
        // Set up language pack
        this.lang(text + '');

        if (this._graphics && this._graphics.replaceText(this._text)) {
            // Replace text successfully and do nothing
            //repaint();
        } else {
            // Typesetting
            this.typeset();
        }
    }
}
Copy code

3. ABCD renderText rendering text

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

    // When overflow is scroll or visible, the line will be truncated
    if (this.overflow != Text.VISIBLE) {
        visibleLineCount = Math.min(
            visibleLineCount,
            Math.floor((this.height - padding[0] - padding[2]) / (this.leading + this._charSize.height)) + 1
        );
    }

    // Clear 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;
    var tCurrBitmapFont: 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];
    }

    // ... omit non critical code

    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++) {
        // ... omit non critical code

        if (tCurrBitmapFont) {
            // ... omit non critical code
            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 font auto scaling
    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 code

Introduction to Laya2.x game engine introduction series

In May of 19, the author began to participate in a OPPO fast game The project (similar to wechat games) has been in the door of H5 game development since its inception. At present, there are not many tutorials about the development of Laya engine fast game, so the author decided to record the pits, problems solved and experience summed up in the past few months, so that other students who are ready to enter the pits can avoid them in advance.

Laya2.x game engine introduction series is expected to write the following articles to record how to develop and launch a fast game from scratch:

At the same time, Laya2 currently refactored the engine code through TypeScript. If you have any questions in writing the code, you can directly GitHub source code I will also write some articles about the source code analysis of Laya2, which can be followed by interested friends.

For the first time, I tried to write a complete teaching article. If there is any mistake or lack of preciseness, please be sure to correct it. Thank you very much!

Recently, we have launched a public official account. If you are interested, we will pay attention to it.

Posted by sicKo on Tue, 28 Apr 2020 23:54:35 -0700