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:
- Normal font
- 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:
- Load font
- Get font name
- 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:
- Set the font to bitmap font directly through Label or Text;
- 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:
- Laya2.x game engine introduction series (1): Hello World
- Laya2.x game engine introduction series (2): UI interface development
- Laya2.x game engine introduction series (3): commonly used animation development
- Laya2.x game engine introduction series (4): pixel level restore text
- Laya2.x game engine introduction series (V): Data Communication [under construction]
- Laya2.x game engine introduction series (6): 2D physical world [under construction]
- Laya2.x game engine introduction series (7): Game debugging [under design]
- Laya2.x game engine introduction series (VIII): project engineering [under construction]
- Laya2.x game engine introduction series (9): the last step of preparation before putting on the shelf [under construction]
- Laya2.x game engine introduction series (10): summary of FAQs [under consideration]
- To be continued...
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.